An interactive, web-based coding playground where students can write and run Node.js (JavaScript) code directly in the browser.
The frontend is built with React + Vite + Monaco Editor, and the backend uses Node.js + Express + VM2 for secure, sandboxed execution.
- server/ – Node/Express API and sandbox execution
index.js– Express app with/api/executeendpoint using VM2package.json– backend scripts and dependencies
- client/ – React + Vite frontend
src/App.tsx– Monaco editor UI, console, and controlssrc/App.css– layout and stylingsrc/main.tsx– React entry point
- Node.js (LTS recommended, e.g. 18+)
- npm (comes with Node)
Make sure you run all commands from the correct folder as shown below.
From the project root:
cd server
npm install # already done once, but safe to run again
npm startThis will:
- Start an Express server on http://localhost:5000
- Expose
POST /api/executewhich accepts:
{
"code": "console.log('Hello from sandbox')"
}and returns:
{
"stdout": "Hello from sandbox",
"stderr": "",
"exitCode": 0,
"runtimeMs": 5,
"timeout": false
}- Code is executed inside a VM2 NodeVM with:
console: 'redirect'– captureconsole.logtimeout: 2000ms – limit long/infinite loopseval,wasm, andrequiredisabled
- Request body size is limited and code length is checked before execution.
In a separate terminal, from the project root:
cd client
npm install # already done once, but safe to run again
npm run devVite will print a URL like:
http://localhost:5173
Open that URL in your browser.
Once both server and client are running:
- Open the frontend in your browser.
- On the left, you’ll see:
- A Monaco editor configured for JavaScript/Node.
- An Examples dropdown (Hello World, Loops, Functions, Arrays).
- Buttons:
- Run (Ctrl+Enter) – sends the current code to
/api/execute. - Clear Output – clears the console (does not clear code).
- Reset Code – resets the editor to the selected example.
- Run (Ctrl+Enter) – sends the current code to
- On the right, you’ll see the Output console:
- stdout – normal program output.
- stderr – errors from the executed code.
- system messages – status and execution time.
Additional behavior:
- Code is automatically saved to
localStorageunder the keysandbox:lastCode. - When the page loads, it restores the last code if available; otherwise, it loads the default example.
- URL:
http://localhost:5000/api/execute - Method:
POST - Headers:
Content-Type: application/json - Body:
{
"code": "// any JavaScript/Node.js code as a string"
}- Response (
200 OKon success):
{
"stdout": "string", // joined console.log output
"stderr": "string", // error message if any
"exitCode": 0, // 0 on success, 1 on error
"runtimeMs": 12, // execution time in milliseconds
"timeout": false // true if the execution hit the timeout limit
}On invalid input or internal errors, the API returns a suitable HTTP status and a simple JSON error message (e.g. { "error": "Code must be a non-empty string." }).
- The app is split into 2 panels:
- Left: Monaco code editor + toolbar (Examples + Run/Clear/Reset).
- Right: Output console that shows
stdout,stderr, and system messages.
code(string): current editor contents. Auto-saved tolocalStorage.output(OutputLine[]): an ordered list of console lines to display in the right panel.- Each line is typed as:
stdout(normalconsole.logoutput)stderr(errors/exceptions)system(status messages like “Running code…” and runtime info)
- Each line is typed as:
isRunning(boolean): disables the Run button while waiting for the backend.selectedExampleId(string): which predefined snippet is currently selected.
- The “Examples” dropdown is backed by a hard-coded array of example snippets.
- Selecting an example:
- updates
selectedExampleId - loads the example’s code into the Monaco editor
- writes a system message like
Loaded example: Hello World
- updates
- Clicking “Reset Code” reloads the selected example into the editor.
-
User edits code in Monaco.
-
User clicks Run (or presses
Ctrl+Enteras mentioned in the UI text). -
The frontend validates basic state (e.g.,
code.trim()must not be empty). -
It sends this request to the backend:
POST http://localhost:5000/api/execute- JSON body:
{ "code": "<editor text>" }
-
The frontend reads the JSON response and renders it to the output console:
- If
stdoutis non-empty, it renders it asstdoutlines. - If
stderris non-empty, it renders it asstderrlines. - It appends a
systemline with execution status +runtimeMs.
- If
- On every
codechange, the app stores the latest code in:localStorage["sandbox:lastCode"]
- On first load:
- if
sandbox:lastCodeexists, it restores it - otherwise it loads the default example
- if
POST /api/execute- Expects JSON:
{ "code": string }
- Produces JSON result:
{ stdout, stderr, exitCode, runtimeMs, timeout? }
The backend rejects requests when:
codeis not a string or is empty/whitespace (400)code.lengthis above the configured limit (413)
This prevents excessive payload sizes from reaching the sandbox.
Backend runs user code inside a VM2 NodeVM configured as:
console: 'redirect'- allows the backend to intercept
console.log(...) - intercepted logs are joined and returned as
stdout
- allows the backend to intercept
timeout: 2000- enforces a maximum execution time inside the VM2 runtime
eval: false,wasm: false- blocks some dynamic/advanced execution paths
require: false- disables module loading from user code
- The server creates a
logs[]array. - It registers a
vm.on('console.log', ...)handler. - When the user code calls
console.log, the handler:- converts arguments to a single string
- pushes into
logs[]
- At the end, the backend returns:
stdout = logs.join('\n')
- The server uses
try/catcharoundvm.run(code, 'user-code.js'). - If an exception occurs:
stderrbecomesString(error)exitCodebecomes1
- If no exception occurs:
stderris returned as an empty stringexitCodebecomes0
- VM2 is configured with
timeout: 2000. - The response includes a
timeoutfield computed using the measured runtime:timeout: runtimeMs >= 2000
- Note: this flag is an approximation based on runtime measurement.
- Start backend:
cd server && npm start- confirm server prints a message like
Sandbox server listening on port 5000
- Start frontend:
cd client && npm run dev- open the Vite URL in the browser
- Pick an example (Hello World) and click
Run. - Confirm:
- output appears in the right console
- errors appear as
stderrwhen code throws
- Test a timeout scenario (example: an infinite loop):
- verify the system shows a timeout-like result (runtime near 2000ms and/or an error).
- Add more predefined examples (async/await, Promises, error handling, data structures).
- Add a snippets manager (save named programs, list and load them).
- Add rate limiting + request logging on the backend.
- Add stronger timeout labeling (detect VM2 timeout exception specifically).
- Package with Docker for easier deployment.
- Frontend cannot reach the server
- Ensure backend is running on
http://localhost:5000. - Check firewall/port conflicts.
- Ensure backend is running on
- Blank page or React errors
- Ensure
npm installfinished successfully inclient/. - Restart
npm run devafter dependency changes.
- Ensure
If you want, I can also add a “Security Considerations” section tailored to your reviewer (what VM2 blocks, what it does not, and why timeouts matter).