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:

  1. The React component holds the code, the chosen language and the input. Run sends them to /api/run on your own server.
  2. That route adds your API key and posts them to https://api.onlinecompiler.io/api/run-code-sync/.
  3. 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?

  1. Get a free API key (100,000 runs a month, no credit card).
  2. Make a React app with Vite and install the editor, its language packages and Express:
Terminal
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
  1. Put the key in .env.local in the project folder. Vite's .gitignore already leaves *.local files out of git. Never name the variable VITE_...: Vite builds those into the code it sends to the browser.
.env.local
ONLINECOMPILER_API_KEY=your_api_key

We 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:

src/App.jsx
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 use extension: [] 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 input World when 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.

src/index.css
body { margin: 0; }
src/App.css
.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
// 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/run holds.
  • 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.json takes 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. Without if (error) throw error the 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:

vite.config.js
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
# 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 dev

For production, build the page once, and the Express server serves it and the route together:

Terminal
npm run build
node --env-file=.env.local server.js   # the app and the route, on http://localhost:3001

The 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:

Output panel
Hello, World!
Exit code 0 in 0.0289 s

And 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.

POST /api/run
{
  "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
// 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.

app/page.tsx
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:

StatusWhenMessage, for example
400The request is wrong: a compiler id the API does not know, empty code, or code over 100 KB.Unsupported or missing compiler
403 or 404The key is missing, mistyped or turned off. Check .env.local and restart the server.Invalid or inactive API key
429A limit is reached: runs a minute, runs at once, or runs this month. The message says which.Submission rate limit exceeded. Try again shortly.
502Sent 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:

429 response
{"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 RapidAPIPublic Piston APIOnlineCompiler.io run-code-sync
RequestPOST https://judge0-ce.p.rapidapi.com/submissionsPOST https://emkc.org/api/v2/piston/executePOST https://api.onlinecompiler.io/api/run-code-sync/
KeyX-RapidAPI-Key and X-RapidAPI-Host headersNone; refused with 401 unless whitelistedAuthorization: YOUR_API_KEY, with no Bearer
Languagelanguage_id: 71language: "python" and versioncompiler: "python-3.14"
Codesource_code, base64 with base64_encoded=truefiles: [{ name, content }]code, as plain text
Inputstdinstdininput
ResultA token to poll with GET /submissions/{token}, or the result at once with wait=trueIn the responseIn the response
Output and errorsstdout, stderr, compile_outputrun.stdout, run.stderroutput, error (compiler messages included)
Successstatus.id 3, Acceptedrun.code 0exit_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}!")

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 widgetYour own React editor
ServerNoneOne route
KeyA widget key in the page, which you can lock to your domainAn API key on your server only
LookOur editor, in a dark, light or automatic themeYour own layout, editor and controls
Who can run codeAnyone on your page, within your account's limitsWhoever your route lets through
The resultIn the page, and in a runcode:result eventIn 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.