How to build an online code editor in React with a Run button
Put a code editor, a language menu, an input box and a Run button in one React component, and have Run send the code to a small route on your own server. That route adds your API key and calls OnlineCompiler.io's run-code-sync endpoint, which runs the program in a sandbox and returns its output, errors and exit code. The key never reaches the browser, and the same component runs any of the 46 languages.
By the OnlineCompiler.io team. Updated .
How does the Run button work?
Three parts pass the code along:
- The React component holds the code, the chosen language and the input. Run sends them to
/api/runon your own server. - That route adds your API key and posts them to
https://api.onlinecompiler.io/api/run-code-sync/. - The API compiles and runs the program in a fresh sandbox with no network access, then answers with what it printed, its errors, its exit code, run time and memory. The route passes the answer back and the component shows it.
The route is there for two reasons. An API key in browser code is public: anyone can copy it from the page and spend your runs. And the API sends no CORS headers, so a browser refuses to call it from your page anyway. If you want an editor with no server at all, the embeddable widget is the simpler choice.
What do I need to set up?
- Get a free API key (100,000 runs a month, no credit card).
- Make a React app with Vite and install the editor, its language packages and Express:
npm create vite@latest code-editor -- --template react
cd code-editor
npm install
npm install @uiw/react-codemirror express \
@codemirror/lang-python @codemirror/lang-javascript @codemirror/lang-cpp @codemirror/lang-java- Put the key in
.env.localin the project folder. Vite's.gitignorealready leaves*.localfiles out of git. Never name the variableVITE_...: Vite builds those into the code it sends to the browser.
ONLINECOMPILER_API_KEY=your_api_keyWe use CodeMirror 6 through @uiw/react-codemirror because it is bundled into your app with no extra
setup; the site's own embeddable editor uses CodeMirror 6 too. To use Monaco
instead, see the questions below.
How do I write the editor component?
Replace src/App.jsx with this component:
import { useState } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { python } from '@codemirror/lang-python';
import { javascript } from '@codemirror/lang-javascript';
import { cpp } from '@codemirror/lang-cpp';
import { java } from '@codemirror/lang-java';
import './App.css';
// Compiler ids from GET https://api.onlinecompiler.io/api/compilers/
const LANGUAGES = [
{
id: 'python-3.14',
label: 'Python (3.14)',
extension: python(),
starter: 'name = input()\nprint(f"Hello, {name}!")\n',
},
{
id: 'nodejs-24',
label: 'JavaScript (Node.js 24)',
extension: javascript(),
starter: "const name = require('fs').readFileSync(0, 'utf8').trim();\nconsole.log(`Hello, ${name}!`);\n",
},
{
id: 'g++-15',
label: 'C++ (G++ 15)',
extension: cpp(),
starter: '#include <iostream>\n#include <string>\n\nint main() {\n std::string name;\n std::getline(std::cin, name);\n std::cout << "Hello, " << name << "!\\n";\n}\n',
},
{
id: 'openjdk-25',
label: 'Java (OpenJDK 25)',
extension: java(),
starter: 'import java.util.Scanner;\n\npublic class Main {\n public static void main(String[] args) {\n String name = new Scanner(System.in).nextLine();\n System.out.println("Hello, " + name + "!");\n }\n}\n',
},
];
export default function App() {
const [language, setLanguage] = useState(LANGUAGES[0]);
const [code, setCode] = useState(LANGUAGES[0].starter);
const [input, setInput] = useState('World');
const [result, setResult] = useState(null);
const [running, setRunning] = useState(false);
function pickLanguage(id) {
const next = LANGUAGES.find((l) => l.id === id);
setLanguage(next);
setCode(next.starter);
setResult(null);
}
async function run() {
setRunning(true);
setResult(null);
try {
const response = await fetch('/api/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ compiler: language.id, code, input }),
});
const data = await response.json().catch(() => ({}));
// A refused run (400, 429, ...) has no output, only an error message.
setResult(response.ok ? data : { refused: data.error || `HTTP ${response.status}` });
} catch {
setResult({ refused: 'Could not reach the server. Is it running?' });
} finally {
setRunning(false);
}
}
return (
<main className="editor">
<div className="toolbar">
<select aria-label="Language" disabled={running} value={language.id} onChange={(e) => pickLanguage(e.target.value)}>
{LANGUAGES.map((l) => (
<option key={l.id} value={l.id}>{l.label}</option>
))}
</select>
<button onClick={run} disabled={running}>{running ? 'Running...' : 'Run'}</button>
</div>
<CodeMirror value={code} height="260px" extensions={[language.extension]} onChange={setCode} />
<label>
Input
<textarea value={input} onChange={(e) => setInput(e.target.value)} rows={3} />
</label>
<section className="output" aria-live="polite">
{result?.refused && <pre className="stderr">{result.refused}</pre>}
{result && !result.refused && (
<>
<pre>{result.output}</pre>
{result.error && <pre className="stderr">{result.error}</pre>}
<p>Exit code {result.exit_code} in {result.time} s{result.truncated ? ' (output cut)' : ''}</p>
</>
)}
</section>
</main>
);
}- The language menu lists compiler ids from
GET https://api.onlinecompiler.io/api/compilers/, which needs no key (see compilers). To add a language, add an entry with its id from the language list and a CodeMirror language package, such as@codemirror/lang-rust; a language without one can useextension: []and shows as plain text. - The input box is the program's standard input. Each starter program reads one line from it,
and each printed
Hello, World!for the inputWorldwhen we ran it. - Run posts the compiler id, the code and the input to
/api/run. A run the API refuses has no output, only an error message, and the panel shows that message. - The output panel shows what the program printed, then its errors in red (standard error, or the compiler's messages when it did not compile), then the exit code and run time.
Give it some room: replace the template's demo styles in src/index.css with the one line below, and
put the rest in src/App.css.
body { margin: 0; }.editor { max-width: 760px; margin: 2rem auto; display: grid; gap: 0.75rem; font-family: system-ui, sans-serif; }
.toolbar { display: flex; gap: 0.5rem; }
.toolbar button { padding: 0.4rem 1.2rem; }
.editor .cm-editor { border: 1px solid #ccc; font-size: 14px; }
.editor label { display: grid; gap: 0.25rem; }
.editor textarea { font-family: ui-monospace, monospace; }
.output { background: #111; color: #eee; padding: 0.75rem; min-height: 4rem; border-radius: 4px; }
.output pre { margin: 0 0 0.5rem; white-space: pre-wrap; }
.output .stderr { color: #ff8a80; }
.output p { margin: 0; color: #aaa; font-size: 0.85rem; }How does the server route keep the API key secret?
Create server.js next to package.json. It is the only part of the app that reads the key:
// server.js: the only part of the app that knows the API key.
import express from 'express';
const API_KEY = process.env.ONLINECOMPILER_API_KEY;
if (!API_KEY) throw new Error('Set ONLINECOMPILER_API_KEY');
const app = express();
app.use(express.json({ limit: '300kb' })); // room for the largest code and input the API takes
app.post('/api/run', async (req, res) => {
const { compiler, code, input } = req.body ?? {};
try {
const response = await fetch('https://api.onlinecompiler.io/api/run-code-sync/', {
method: 'POST',
headers: { Authorization: API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ compiler, code, input }),
signal: AbortSignal.timeout(60_000),
});
// Pass the answer through: 200 with the result, or 400 or 429 with {"error": "..."}.
res.status(response.status).type('json').send(await response.text());
} catch {
res.status(502).json({ error: 'The code runner did not answer. Try again.' });
}
});
app.use(express.static('dist')); // the built React app, after npm run build
const port = process.env.PORT || 3001;
app.listen(port, (error) => {
if (error) throw error;
console.log(`Listening on http://localhost:${port}`);
});- It sends only the compiler, the code and the input, whatever else a request to
/api/runholds. - It passes the API's status and JSON back unchanged, so the component shows the API's own message for a refused
run. A program that fails still answers 200, with its errors in
error. express.jsontakes up to 300 kB, because Express's default of 100 kB would refuse the largest program the API accepts (100 KB of code and 100 KB of input) before it got there.- Express 5 hands a failed
listen, such as a port already in use, to the callback. Withoutif (error) throw errorthe server prints that it is listening and then quietly exits.
During development Vite serves the page itself, so tell it to send /api requests on to Express:
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react()],
// In development, send /api requests to the Express server.
server: { proxy: { '/api': 'http://localhost:3001' } },
});Then start both and open http://localhost:5173. The --env-file flag needs Node 20.6 or newer:
# Terminal 1: the route, on port 3001
node --env-file=.env.local server.js
# Terminal 2: the React app, on http://localhost:5173
npm run devFor production, build the page once, and the Express server serves it and the route together:
npm run build
node --env-file=.env.local server.js # the app and the route, on http://localhost:3001The build may warn that a chunk is larger than 500 kB after minification. That is advice about splitting the bundle, not an error.
We built this app with Vite 8, React 19 and Express 5 and pressed Run with the Python starter and the input World. The panel showed:
Hello, World!
Exit code 0 in 0.0289 sAnd this is the JSON the route passed back from the API. time is the run time in seconds and memory is in KB; every field is described in the run-code
docs.
{
"output": "Hello, World!\n",
"error": "",
"status": "success",
"exit_code": 0,
"signal": null,
"truncated": false,
"time": "0.0289",
"memory": "14408",
"total": "0.0511",
"files": [],
"files_truncated": false,
"time_limit": 30,
"memory_limit": 512
}If you would rather use a package, the official Node.js client (npm install onlinecompiler, Node 22.13 or newer) reads ONLINECOMPILER_API_KEY itself, and client.run({ compiler, code, input }) in the
route returns the same result. A refused run throws instead, a RateLimitError for 429, with the
API's message.
How do I do the same in Next.js?
In a Next.js App Router project, the route is a route handler:
// app/api/run/route.ts: the only part of the app that knows the API key.
export async function POST(request: Request) {
const key = process.env.ONLINECOMPILER_API_KEY;
if (!key) return Response.json({ error: 'Server is missing ONLINECOMPILER_API_KEY' }, { status: 500 });
const body = await request.json().catch(() => null);
const { compiler, code, input } = body ?? {};
try {
const response = await fetch('https://api.onlinecompiler.io/api/run-code-sync/', {
method: 'POST',
headers: {
Authorization: key,
'Content-Type': 'application/json',
},
body: JSON.stringify({ compiler, code, input }),
signal: AbortSignal.timeout(60_000),
});
// Pass the answer through: 200 with the result, or 400 or 429 with {"error": "..."}.
return new Response(await response.text(), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
});
} catch {
return Response.json({ error: 'The code runner did not answer. Try again.' }, { status: 502 });
}
}Put the key in .env.local as above: Next.js loads it, and its .gitignore already leaves .env* files out of git. The component works as it is with 'use client'; as its first
line: save it as app/CodeEditor.jsx with App.css beside it, and render it from the
page. No proxy is needed, because the page and the route share one server.
import CodeEditor from './CodeEditor';
export default function Page() {
return <CodeEditor />;
}We built this with Next.js 16.4; next build type-checks the route handler. Pressing Run with the
Python starter printed the same Hello, World!.
What happens when the code fails or a limit is reached?
The program ran but failed. The API answers 200, exit_code is not 0, and error holds the program's standard error or the compiler's messages, which the panel shows in red.
Exit code 124 means the run went over 30 seconds, and 137 that it went over
512 MB of memory; the exit code table lists the rest.
The run was refused. The API answers an error status with {"error": "..."},
nothing runs, and it does not count against your runs. The component shows the message:
| Status | When | Message, for example |
|---|---|---|
| 400 | The request is wrong: a compiler id the API does not know, empty code, or code over 100 KB. | Unsupported or missing compiler |
| 403 or 404 | The key is missing, mistyped or turned off. Check .env.local and restart the server. | Invalid or inactive API key |
| 429 | A limit is reached: runs a minute, runs at once, or runs this month. The message says which. | Submission rate limit exceeded. Try again shortly. |
| 502 | Sent by your own route when it could not reach the API, or got no answer within 60 seconds. | The code runner did not answer. Try again. |
On the Free plan, a 429 for too many runs a minute names the plan that allows more:
{"error": "Submission rate limit exceeded. Try again shortly. Starter allows 60 a minute: https://api.onlinecompiler.io/billing/?plan=starter"}The Free plan has 100,000 runs a month, 30 a minute and 2 at once, and every run gets 512 MB of memory and up to 30 seconds; the monthly count starts again on the first of the month (UTC). All of it is on the limits page.
Anyone who can open your page can press Run, and anyone who finds /api/run can call it. On a public
site, put the route behind your own sign-in or a rate limit, and give the key a monthly limit of its own on the
API keys page of your dashboard, so it cannot use up the whole account.
How do I switch from Judge0 on RapidAPI or the public Piston API?
The tutorials that come up when you search for this call one of these two. The GeeksforGeeks tutorial (updated 23 July 2025) posts the code from its Express route to the public Piston API, and the freeCodeCamp tutorial (24 May 2022) calls Judge0 CE on RapidAPI from the browser, with
the RapidAPI key in REACT_APP_ variables, which are built into the code the browser downloads. Since
15 February 2026 the public Piston API answers 401 "whitelist only", and
it still did when we checked on 6 November 2026. This is how their requests map to run-code-sync:
| Judge0 CE on RapidAPI | Public Piston API | OnlineCompiler.io run-code-sync | |
|---|---|---|---|
| Request | POST https://judge0-ce.p.rapidapi.com/submissions | POST https://emkc.org/api/v2/piston/execute | POST https://api.onlinecompiler.io/api/run-code-sync/ |
| Key | X-RapidAPI-Key and X-RapidAPI-Host headers | None; refused with 401 unless whitelisted | Authorization: YOUR_API_KEY, with no Bearer |
| Language | language_id: 71 | language: "python" and version | compiler: "python-3.14" |
| Code | source_code, base64 with base64_encoded=true | files: [{ name, content }] | code, as plain text |
| Input | stdin | stdin | input |
| Result | A token to poll with GET /submissions/{token}, or the result at once with wait=true | In the response | In the response |
| Output and errors | stdout, stderr, compile_output | run.stdout, run.stderr | output, error (compiler messages included) |
| Success | status.id 3, Accepted | run.code 0 | exit_code 0 |
Or keep the request you already have, because the API answers both shapes. Point Judge0 code at https://api.onlinecompiler.io/api/judge0 with your key in X-Auth-Token (Judge0-compatible
API), or Piston code at https://api.onlinecompiler.io/api/v2/piston with it in Authorization (Piston-compatible API). Either way, move the call into your server route: a key in REACT_APP_ or VITE_ variables is public. The Judge0
migration guide covers language ids and the other differences.
When is the embeddable widget simpler?
When all you need is an editor with a Run button on a page, the embeddable code editor needs no server and no route. Its widget key sits in the page's HTML: it only works for the editor, cannot call the REST API, and can be locked to your domain. This is the widget, running the same Python starter:
name = input()
print(f"Hello, {name}!") World
In a React app, load https://onlinecompiler.io/widget.js once in index.html and render a div with the class runcode, your widget key in data-key and a compiler id
in data-lang. The script starts editors that appear after the page has loaded, so a component can
render one at any time. The widget docs cover the attributes and its events.
| Embeddable widget | Your own React editor | |
|---|---|---|
| Server | None | One route |
| Key | A widget key in the page, which you can lock to your domain | An API key on your server only |
| Look | Our editor, in a dark, light or automatic theme | Your own layout, editor and controls |
| Who can run code | Anyone on your page, within your account's limits | Whoever your route lets through |
| The result | In the page, and in a runcode:result event | In your component and on your server |
Build your own, as this guide does, when the editor is part of your app: your own layout, your own sign-in in front of Run, or the result on your server to save or grade. To grade it, send the code to the run-tests API with expected outputs, as the online compiler with test cases does.
Frequently asked questions
Can I call the API straight from React, without a server?
No. An API key in browser code can be read by anyone who opens the page, and the API sends no CORS headers, so the browser blocks the request anyway. Keep the key in a server route, or use the embeddable widget, whose widget key is meant to sit in public HTML.
Can I use Monaco, the VS Code editor, instead of CodeMirror?
Yes. Swap the CodeMirror element for Editor from @monaco-editor/react, give each language Monaco's name (python, javascript, cpp, java), and call setCode(value ?? '') in its onChange. By default it downloads Monaco from a CDN when the page opens. The route and the rest of the component stay the same.
Which languages can my editor run?
Any of the 46 languages the API runs. GET https://api.onlinecompiler.io/api/compilers/ lists every compiler id, needs no key, and is the list to build your language menu from. Python comes with numpy, pandas and other data libraries installed.
Is it free?
Yes, up to 100,000 runs a month and 30 a minute on the Free plan, commercial use included, with no credit card. A run refused with 429 is not counted. Paid plans raise the monthly runs and the per-minute rate.
How do I check whether the code in the editor is right?
Send it to the run-tests endpoint with test cases instead of run-code-sync. Each test has an input and an expected output, and the answer gives a verdict for each test and a score. Tests can be hidden, so a student sees the result without the expected output.