frontend
LetCode
Browser-based coding practice for interview prep: 11 tracks, 55 problems, 443 test cases, and a judge that runs entirely on the user's device with no execution server.
- Role
- Frontend (with a serverless visitor counter)
- Duration
- October 2026
- Date
- October 2026
Technologies
- nuxt
- vue
- typescript
- unocss
- monaco-editor
- pyodide
- web worker
- netlify functions
- netlify blobs
- markdown-it
- eslint
- pnpm
Impact
11 tracks, 55 problems, and 443 test cases, all graded in the browser with zero execution servers.
Problem
Interview practice asks for three things at once: an ordered curriculum, somewhere to write code, and objective grading. LeetCode has all three, but it is in English and its curriculum is not always aligned for someone just starting. Local material usually stops at theory without automatic grading, so readers never find out whether their solution is correct.
Building it yourself has a different set of traps: grading code almost always means a server, a sandbox, and a monthly bill. Other people’s code must not run on the same machine as your data. This project was locked to two constraints that could not be broken: zero cost, and no server that executes code.
Solution
All execution happens in the browser. JavaScript runs in a fresh Web Worker for every attempt, and the worker is terminated when it exceeds the time limit, the only way to stop an infinite loop in a browser. Python uses Pyodide in a separate worker that is deliberately not shut down between executions, because reloading the Python runtime costs far more than clearing its variables. Two languages, two strategies, because their cost profiles are opposite.
Problems use function signatures instead of stdin/stdout. That decision simplifies almost everything: no fake terminal, no output parsing, and the system only has to rearrange the participant’s code to take the target function and call it with JSON arguments. Data structures travel as JSON representations, a binary tree as a level-order array with nulls and a linked list as an array, so participants still have to build the structure themselves inside their code.
The architecture is split into four layers with enforced dependency rules: the UI never calls a Worker directly, and the runner layer never touches localStorage. The runner layer sits outside Nuxt’s auto-import folder so every use has to go through an explicit import, a layer boundary that cannot be crossed by accident.
Because the site is static, content is the only product differentiator. So the build runs a validation pipeline that executes every JavaScript and Python reference solution against all of its test cases. One failing problem fails the build. But that validation is circular: it proves the solution matches the expected value, not that the expected value is right. If both are wrong, the pipeline still passes.
So I wrote an independent solver for all 55 problems, built from the problem specification without reading the reference solution, then compared it with every expected value. The result was 443 test cases with no differences, and that is where the real bugs surfaced. The “order does not matter” comparator sorted at every level, not only at the collection level. As a result, permutations accepted an answer containing six copies of the same permutation, and k-closest-points accepted flipped coordinates, mathematically different points. The fix was to separate two semantics: free order at the top level only, versus free order at every level for results that are truly sets.
The second bug came from the worker boundary. Pyodide’s built-in conversion is not symmetric for empty values: JavaScript null becomes the JsNull sentinel instead of None, and Python None comes back as undefined instead of null. As a result, binary-tree problems could not be graded correctly in Python. The fix was to make JSON the conversion bridge in both directions, and since then null is safe to use in expected values.
The interface uses Monaco with its worker bundled separately, and Pyodide is served from its own static folder so there is no CDN dependency at runtime. Two interface languages, Indonesian and English, are fully translated including all problem content and lessons, without an i18n library. The English dictionary is declared as a type that must be complete, so a missing translation fails at compile time, not when a user opens it.
The only server-side component is the visitor counter. Netlify Blobs was chosen because it is already the same provider as the hosting, but two details shaped the implementation: Blobs defaults to eventual consistency, which can make the number look stale for up to 60 seconds, and Blobs has no atomic increment operation. So the store is opened with strong consistency, and writes use an etag-based conditional write with retries: better to lose one count than to display a wrong number.
The final result is 11 tracks from Arrays & Hashing to Backtracking, 55 problems in two programming languages and two interface languages, with 20 easy, 28 medium, and 7 hard problems. The validation pipeline rejects the build if any problem is broken, and 20 separate checks cover visitor-counter behavior including conflict handling and corrupted data.
Contact
Interested in working together?
Tell me what you are building and what you need. A short note is enough.