Stack QuestJoin the beta

15 programs you build yourself, and keep.

A project is not a video to watch or a repository to clone. Each one is a handful of functions you write, in order, where every function builds on the one before it and is checked by tests that run on your phone. All projects use JavaScript and require a Full Access subscription.

Projects
15
Steps
75
Tests
301
Minutes
≈385start to finish, all five
Service code

Build a worker that retries safely

Capped backoff, due-time filtering, a retry budget that ends in a dead letter, idempotency, and one testable tick of a worker.

What you end up with

The worker loop behind every background queue, including the three mistakes that turn one failing dependency into an outage.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Work out how long to waitbackoffMs()

    Complete backoffMs(attempts, baseMs, capMs). The first attempt waits baseMs, and each attempt after doubles it, never exceeding capMs. Treat attempts below 1 as 1.

  2. 2

    Pick out the jobs that are actually duedueJobs()

    Complete dueJobs(jobs, now). Each job is { id, state, readyAt, attempts }. Return the jobs whose state is 'ready' and whose readyAt is at or before now, in their original order.

  3. 3

    Decide a job's fate after a failureafterFailure()

    Complete afterFailure(job, now, maxAttempts, baseMs, capMs). Increment attempts. When it reaches maxAttempts, return the job with state: 'dead' and readyAt unchanged. Otherwise return it 'ready' with readyAt set to now plus backoffMs(attempts, baseMs, capMs).

  4. 4

    Do the same work twice without doing it twicerunOnce()

    Complete runOnce(seen, key, result). seen maps key to a stored result. When key is already there, return { seen, result: stored, ran: false } with seen unchanged. Otherwise record result under key and return { seen, result, ran: true }.

  5. 5

    Run one tick of the workertick()

    Complete tick(jobs, now, capacity, outcomes, limits). outcomes maps job id to true for success or false for failure; limits is { maxAttempts, baseMs, capMs }. Take at most capacity due jobs in order. A success becomes state: 'done'; a failure goes through afterFailure. Return the full job list in its original order, with untouched jobs unchanged.

System design

Build a cache that forgets

Five steps that turn a plain object into a cache with expiry, a size cap and least-recently-used eviction.

What you end up with

A cache a real service could use: entries that expire, a hard size limit, least-recently-used eviction, and hit and miss counts that show whether it is earning its keep.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Remember a valuecacheSet()

    Complete cacheSet(cache, key, value, now). cache is { ttlMs, maxEntries, entries }. Store { value, storedAt: now, usedAt: now } under key in cache.entries, replacing anything already there, and return the cache.

  2. 2

    Stop serving stale answerscacheGet()

    Complete cacheGet(cache, key, now). Return null if key is not in cache.entries, or if now - storedAt is at least cache.ttlMs — in that case delete the entry first. Otherwise set the entry’s usedAt to now and return its value.

  3. 3

    Cap how much it holdscachePut()

    Complete cachePut(cache, key, value, now). Store the entry with cacheSet, then while cache.entries holds more than cache.maxEntries keys, delete the entry with the smallest storedAt. Return the cache.

  4. 4

    Evict the one nobody wantsevictLru()

    Complete evictLru(cache). While cache.entries holds more than cache.maxEntries keys, delete the entry with the smallest usedAt; if two tie, the key added first is the one that goes. Return the cache. Then change cachePut to call evictLru instead of dropping the oldest entry.

  5. 5

    Count the hits and missesrunCache()

    Complete runCache(cache, ops). Each op is ["set", key, value, now] or ["get", key, now]; run sets through cachePut and gets through cacheGet, collecting each get’s return value in order. Return { results, hits, misses, hitRate, keys }, where a get returning null is a miss, hitRate is hits divided by the number of gets rounded to two decimals (0 when there were no gets), and keys is Object.keys(cache.entries).

System design

Build a rate limiter

Five steps from a fixed window that leaks bursts to a per-caller token bucket a service could run on.

What you end up with

A working token-bucket rate limiter: buckets that refill over time, one per caller, and a decision function that answers allowed, how many left, and when to retry.

  • 5 steps
  • 20 tests
  • ≈30 minutes
  • javascript
  1. 1

    Count hits in a windowfixedWindow()

    Complete fixedWindow(state, now, limit, windowMs). state is { windowStart, count } and now is a timestamp in milliseconds. The window now belongs to starts at Math.floor(now / windowMs) * windowMs; if that is not state.windowStart, the count restarts at 0. Return { allowed, windowStart, count }: when the count so far is below limit, allowed is true and count goes up by one, otherwise allowed is false and count is unchanged.

  2. 2

    Watch a burst slip throughreplayFixed()

    Complete replayFixed(times, limit, windowMs). times is an array of arrival timestamps in milliseconds, in order. Start from the state { windowStart: -1, count: 0 } — no real window starts before 0 — and feed each timestamp through fixedWindow, carrying the windowStart and count it returns into the next call. Return { allowed, denied }: how many arrivals were allowed and how many were refused. Call fixedWindow rather than repeating its logic.

  3. 3

    Refill a bucket over timerefill()

    Complete refill(bucket, now, capacity, refillMs). bucket is { tokens, last } — tokens on hand, and the timestamp they were last counted at. One whole token arrives every refillMs milliseconds, so the bucket gains Math.floor((now - bucket.last) / refillMs) tokens, capped at capacity. Move last forward by exactly that many refill intervals, not to now. Return the new { tokens, last }; if that gain is zero or less, return the same tokens and last you were given.

  4. 4

    One bucket per callerbucketFor()

    Complete bucketFor(buckets, key, now, capacity, refillMs). buckets maps a caller key to that caller's { tokens, last }. Return the bucket for key, refilled up to now with refill. A key that is not in buckets yet starts full, as { tokens: capacity, last: now }. Return the bucket only, and leave buckets itself alone.

  5. 5

    Allowed, or come back latercheck()

    Complete check(buckets, key, now, capacity, refillMs). Get the caller's refilled bucket from bucketFor. If it holds at least one token, spend one and return { allowed: true, remaining, retryAfterMs: 0 }, where remaining is what is left after spending. If it holds none, return { allowed: false, remaining: 0, retryAfterMs }, where retryAfterMs is bucket.last + refillMs - now: the wait until the next token lands. Either way, store the bucket you ended up with back into buckets under key so the next call sees it.

DevOps & Cloud

Build a rollout controller

Health as a share, batches that bound the blast radius, a canary judged against the running version, and a rollout that stops.

What you end up with

The deploy logic behind a rolling update, including the decision that separates a bad build from an outage.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Decide whether a thing is healthyhealthy()

    Complete healthy(checks, threshold). checks is an array of booleans. Return true when the share passing is greater than or equal to threshold. An empty array is never healthy.

  2. 2

    Roll out in batches, not all at oncebatches()

    Complete batches(instances, size). Return an array of arrays, splitting instances into consecutive groups of at most size, in order. Return [] for no instances.

  3. 3

    Judge the canarycanaryOk()

    Complete canaryOk(canaryErrorRate, baselineErrorRate, tolerance). Return true when the canary's error rate is at most the baseline plus tolerance, and false otherwise.

  4. 4

    Know what to undorollbackPlan()

    Complete rollbackPlan(before, after). Both map instance name to version. Return an array of { instance, to } for every instance whose version differs, in the order the keys appear in before, sending each back to its before version.

  5. 5

    Run the rolloutrollout()

    Complete rollout(plan). plan is { instances, size, threshold, canary, health } where canary is { rate, baseline, tolerance } and health is an array of check-arrays, one per batch. Return { deployed, status }. When the canary fails return { deployed: [], status: 'canary refused' }. Otherwise deploy batch by batch, adding each batch's instances to deployed, and stop with 'rolled back' at the first unhealthy batch — its instances still count as deployed. Finish with 'complete'.

Data Structures & Algorithms

Build a text search index

Five functions that turn a pile of documents into a search box that ranks what it finds.

What you end up with

A working search index: a tokenizer, an inverted index, term lookup, frequency ranking, and multi-word queries that return the best document first.

  • 5 steps
  • 20 tests
  • ≈30 minutes
  • javascript
  1. 1

    Break the text into wordstokenize()

    Complete tokenize(text). Return an array of the words in text: lowercased, split on every run of characters that is not a letter or digit, with empty strings dropped. Text with no letters or digits returns [].

  2. 2

    Turn the documents inside outbuildIndex()

    Complete buildIndex(docs). docs is an array of { id, text }. Return an object mapping each token to an object mapping document id to the number of times that token appears in that document. Use tokenize. An empty docs returns {}.

  3. 3

    Look a word uplookup()

    Complete lookup(index, term). Run term through tokenize and take the first token. Return the document ids listed under that token, in the order they appear in the index entry. Return [] if the term has no token, or the index has no entry for it.

  4. 4

    Put the best result firstrank()

    Complete rank(index, term). Return an array of { id, score } for every document lookup finds, where score is that document’s count for the term. Sort by score descending, breaking ties by id ascending. No hits returns []. Use lookup rather than reading the index twice.

  5. 5

    Answer a real querysearch()

    Complete search(docs, query). Tokenize query, build the index from docs, and return { id, score } for every document containing every one of the query’s tokens, where score is the sum of that document’s counts for them. Sort by score descending, ties by id ascending. A query with no tokens returns []. Use the functions you already wrote.

Generative AI

Build the retrieval half of RAG

Five steps that turn a document into a prompt: chunking, an inverted index, scoring, ranking, and a prompt that cites what it used.

What you end up with

The whole retrieval path a RAG system runs before any model is involved — and the part that decides whether the answer can be grounded at all.

  • 5 steps
  • 19 tests
  • ≈25 minutes
  • javascript
  1. 1

    Cut the document into chunkschunkWords()

    Complete chunkWords(text, size). Split text on whitespace, drop empty pieces, and return an array of strings of at most size words each, in order, each joined by single spaces. Return [] for empty text.

  2. 2

    Index which chunk each word is inbuildIndex()

    Complete buildIndex(chunks). Return an object mapping each lowercased word to a sorted array of the chunk indexes it appears in. Split words on anything that is not a letter or digit, drop empty pieces, and list each chunk index at most once per word.

  3. 3

    Score a chunk against a questionscoreChunk()

    Complete scoreChunk(index, chunkIndex, question). Split question into lowercased words the same way buildIndex does, and return how many *distinct* question words have chunkIndex listed against them in index.

  4. 4

    Rank, and refuse to padtopChunks()

    Complete topChunks(index, chunkCount, question, k). Score every chunk index from 0 to chunkCount - 1 with the same rule as scoreChunk, drop any that score 0, sort by score highest first with the lower chunk index winning a tie, and return at most k chunk indexes.

  5. 5

    Assemble a prompt that cites its sourcesbuildPrompt()

    Complete buildPrompt(chunks, picked, question). When picked is empty, return exactly The documents do not cover this.. Otherwise return a string: one line per picked chunk as [n] <chunk text> numbered from 1 in the order given, then a blank line, then Question: <question>, then Answer using only the numbered context above, and cite it.

Git & collaboration

Build a commit graph

Five steps that build the data structure git actually is: commits with parents, reachability, merge-base, and the three kinds of merge.

What you end up with

A working model of history that explains fast-forwards, merge bases and why a commit nothing points at eventually disappears.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Record a commitcommit()

    Complete commit(graph, id, parents, message). Store { parents, message } under id in graph and return the graph. A commit with no parents is the first one; a commit with two is a merge.

  2. 2

    Walk back through historyancestors()

    Complete ancestors(graph, id). Return every commit id reachable from id including id itself, sorted alphabetically, with no duplicates. Ignore ids the graph does not know.

  3. 3

    Answer whether one commit contains anothercontains()

    Complete contains(graph, id, candidate). Return true when candidate is reachable from id, including when they are the same commit, and false otherwise.

  4. 4

    Find where two branches partedmergeBase()

    Complete mergeBase(graph, a, b). Return the commit nearest to a — fewest steps back — that is also reachable from b, or null when the two share no history. Break a distance tie by taking the alphabetically smaller id.

  5. 5

    Decide what kind of merge this ismergeKind()

    Complete mergeKind(graph, mine, theirs). Return 'up to date' when mine already contains theirs, 'fast forward' when theirs contains mine, 'unrelated' when they share no history, and 'merge' otherwise. Identical commits are up to date.

JavaScript

Build a receipt printer

Five functions that add up to a receipt you could hand someone.

What you end up with

A working receipt printer in exact whole cents: line totals, a subtotal, tax, money formatting, and the function that prints the lot.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    One line of the receiptlineTotal()

    Complete lineTotal(item). item is { name, price, qty }, and price is in whole cents — 3.50 arrives as 350. Return price × qty in cents.

  2. 2

    Add the lines upsubtotal()

    Complete subtotal(items). items is an array of { name, price, qty } with prices in cents. Return the sum of every line total, in cents. Use lineTotal rather than repeating its maths.

  3. 3

    Charge the taxwithTax()

    Complete withTax(amount, rate). amount is in cents and rate is a fraction, so 8% arrives as 0.08. Return the amount plus its tax, with the tax rounded to the nearest cent.

  4. 4

    Make it look like moneymoney()

    Complete money(cents). Return the amount as dollars: a leading $ and exactly two decimal places, so 1150 shows as $11.50.

  5. 5

    Print the whole thingprintReceipt()

    Complete printReceipt(items, rate). Return an array of strings: one line per item as NAME xQTY $AMOUNT, then Subtotal $X, Tax $X, and Total $X. Tax is the difference between the subtotal and the taxed total, in cents. Use the functions you already wrote.

Machine learning

Build k-nearest neighbours

Five steps that build a working classifier from distance alone — and the scaling bug that silently ruins it.

What you end up with

A classifier you can explain line by line, scored on data it never saw.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Measure how far apart two examples aredistance()

    Complete distance(a, b). Both are arrays of numbers of the same length. Return the sum of (a[i] - b[i]) squared over every index — the squared Euclidean distance, with no square root.

  2. 2

    Find the k closestnearest()

    Complete nearest(rows, point, k). Each row is { features, label }. Return the k rows closest to point by squared distance, closest first, with the earlier row winning a tie. Return every row when k exceeds the number of rows.

  3. 3

    Let the neighbours votevote()

    Complete vote(rows). Each row is { features, label }. Return the label appearing most often. When two labels tie, return the one whose first appearance in rows is earlier. Return null for no rows.

  4. 4

    Put the features on the same scalenormalise()

    Complete normalise(rows). Each row is { features, label }. Return new rows whose every feature is scaled to (value - min) / (max - min) using that feature column's min and max across all rows. When a column's min equals its max, use 0. Leave labels untouched.

  5. 5

    Score the model honestlyaccuracy()

    Complete accuracy(train, test, k). For each row in test, predict its label with vote(nearest(train, row.features, k)), and return the share predicted correctly as a number between 0 and 1. Return 0 for an empty test set.

Python

Build a word-frequency report

Raw text to a readable report: deciding what a word is, counting, ranking with repeatable ties, and aligning the columns.

What you end up with

The counting-and-reporting pipeline behind most first data tasks, including the two choices that decide whether the same text always produces the same report.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Split text into wordswords()

    Complete words(text). Lowercase the text and split it on any run of characters that is not a letter, digit or apostrophe. Drop empty pieces and return the words in order.

  2. 2

    Count how often each word appearscounts()

    Complete counts(list). Return an object mapping each distinct word to how many times it appears. Return {} for an empty list.

  3. 3

    Rank them, and settle the tiestopWords()

    Complete topWords(tally, n). Return the n most frequent words as { word, count }, most frequent first, breaking ties alphabetically.

  4. 4

    Line up the columnsformatRow()

    Complete formatRow(word, count, width). Return the word padded on the right with spaces to at least width characters, then a single space, then the count. A word longer than width is not truncated.

  5. 5

    Print the reportreport()

    Complete report(text, n). Produce the top n words and return one line per word from formatRow, joined by newlines, with the width set to the longest word *among those shown*. Return 'no words' when the text has none.

Security

Build a token gate

An untrusted string to an authorisation decision: shape, a constant-time signature check, expiry, scope, and a refusal that says why.

What you end up with

The gate every authenticated request passes through, including the two mistakes that make one useless — an early-returning comparison and a check that stops at 'is this a real user'.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Split the token, and refuse anything oddparseToken()

    Complete parseToken(raw). A token is payload.signature. Return { payload, signature } when raw is a string of exactly two non-empty dot-separated parts, and null for anything else.

  2. 2

    Compare signatures without leaking the answersameSignature()

    Complete sameSignature(a, b). Return true when the two strings are identical. Different lengths return false immediately, but when the lengths match, examine every character rather than stopping at the first difference.

  3. 3

    Refuse a token that has run outisLive()

    Complete isLive(payload, now). payload is an object. Return true only when payload.expiresAt is a number strictly greater than now. Anything missing, non-numeric or in the past is not live.

  4. 4

    Grant only what was asked forhasScope()

    Complete hasScope(payload, needed). Return true only when payload.scopes is an array containing exactly the string needed. A missing list, or a near-match like read:all for read, is not permission.

  5. 5

    Put the whole check togetherauthorise()

    Complete authorise(raw, expectedSignature, payload, now, needed). Return { ok: true } when the token parses, its signature matches expectedSignature, the payload is live and it carries needed. Otherwise return { ok: false, reason } with reason one of 'malformed', 'bad signature', 'expired' or 'forbidden', checked in that order.

Databases & SQL

Build a tiny query engine

Five steps that turn arrays of objects into WHERE, ORDER BY, GROUP BY and JOIN — and show why a database applies them in that order.

What you end up with

A query engine you can read end to end, and a working answer to why filtering comes before sorting.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Keep only the rows that matchwhere()

    Complete where(rows, field, op, value). rows is an array of objects and op is one of '=', '>' or '<'. Return a new array of the rows whose field satisfies the comparison. Return [] for any other operator.

  2. 2

    Put them in order, and keep ties stableorderBy()

    Complete orderBy(rows, field, direction). direction is 'asc' or 'desc'. Return a new array sorted by field, and when two rows compare equal keep the one that came first in rows first — in both directions.

  3. 3

    Collapse rows into groupsgroupCount()

    Complete groupCount(rows, field). Return an array of { key, count } — one entry per distinct value of field, counting the rows holding it, in the order each value first appears in rows.

  4. 4

    Join two tablesjoin()

    Complete join(left, right, leftField, rightField). Return one merged object for every pair where left[leftField] === right[rightField], left row first then right row's fields on top, in left-then-right order. A left row with no match contributes nothing.

  5. 5

    Run the whole queryrunQuery()

    Complete runQuery(rows, query). query is { where, orderBy, limit }; each part may be missing. Apply where as { field, op, value } first, then orderBy as { field, direction }, then return at most limit rows. Missing parts are skipped.

Testing & craft

Build a test runner

Five functions that grow into the runner every testing framework is underneath.

What you end up with

A working test runner: deep equality, assertions that answer instead of throwing, a whole suite counted, and a failure report you can read.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Tell whether two values matchdeepEqual()

    Complete deepEqual(a, b). Return true when the two values are equal: identical primitives, arrays of the same length whose items are all deep-equal, or objects with the same set of keys whose values are all deep-equal. Everything else is false, including an array compared with a plain object.

  2. 2

    Answer instead of throwingcheck()

    Complete check(name, actual, expected). Return { name, pass, actual, expected } — the same four keys whether it passed or failed — where pass is true only when actual and expected are deep-equal. Use deepEqual; never throw.

  3. 3

    Run one test caserunCase()

    Complete runCase(testCase). A case is { name, actual, expected }, or { name, error } when the code under test threw instead of returning. If the case has an error property, return { name, pass: false, error } and compare nothing; otherwise return the result of check.

  4. 4

    Run the whole suiterunSuite()

    Complete runSuite(cases). Run each case with runCase, in order, and return { total, passed, failed, results } — total is how many cases ran, passed how many have pass: true, failed the rest, and results the array of case results in the original order.

  5. 5

    Say what failed and whyreport()

    Complete report(cases). Run them with runSuite, then return an array of strings: for each failing result, in order, FAIL <name>, then either the single line threw: <error> when the case errored, or the two lines expected: <JSON> and actual: <JSON>. Passing results add no lines. End the array with one summary line, <passed> passed, <failed> failed, <total> total. Detail lines start with two spaces, and values are rendered with JSON.stringify.

TypeScript

Build a type checker

What TypeScript does to your code: literal types, operator rules, narrowing a union, checking a call, and walking an expression.

What you end up with

A working answer to why `1 - 'b'` and `1 === 'b'` are errors in TypeScript while JavaScript runs them, written as the checker that decides it.

  • 5 steps
  • 22 tests
  • ≈25 minutes
  • javascript
  1. 1

    Name the type of a valuetypeOf()

    Complete typeOf(value). Return 'null', 'number', 'string' or 'boolean' for those values, and 'unknown' for anything else.

  2. 2

    Decide what an operator producescheckBinary()

    Complete checkBinary(op, left, right) where left and right are type names. '+' gives 'string' when either side is a string, 'number' for two numbers, and a type error otherwise. '-' and '*' need two numbers and give 'number'. '===' gives 'boolean' when the two types can overlap — the same type, or either side 'null' — and a type error when they cannot. Anything else returns null, meaning a type error.

  3. 3

    Narrow a unionnarrow()

    Complete narrow(union, guard). union is an array of type names and guard is the type a typeof check proved. Return the array of members equal to guard. An empty result means the branch is unreachable.

  4. 4

    Check a call against its signaturecheckCall()

    Complete checkCall(signature, args). signature is { params, returns } where params is an array of type names. Return { ok: false, reason: 'arity' } when the counts differ, { ok: false, reason: 'argument ' + i } for the first mismatched position counting from 0, and { ok: true, type: returns } when every argument fits.

  5. 5

    Check a whole expressioncheckExpr()

    Complete checkExpr(node). A node is { kind: 'literal', value } or { kind: 'binary', op, left, right }. A literal's type comes from typeOf. A binary checks both sides first and returns the first error it finds, otherwise checkBinary's result — returning { error: 'type error' } when that is null. Return the type name on success.

Web & APIs

Build an HTTP router

A raw URL to a response: segments, patterns with parameters, the query string, honest status codes, and 405 instead of 404.

What you end up with

The routing layer every web framework hides, including the two choices that decide how kind your API feels to use.

  • 5 steps
  • 20 tests
  • ≈25 minutes
  • javascript
  1. 1

    Cut a path into segmentssegments()

    Complete segments(path). Return the path's non-empty parts in order, so '/users/7/' gives ['users','7'] and '/' gives [].

  2. 2

    Match a pattern and pull out the parametersmatchRoute()

    Complete matchRoute(pattern, path). A pattern segment beginning : captures that path segment by name. Return an object of captured parameters when every segment matches, or null when the route does not match at all.

  3. 3

    Read the query stringparseQuery()

    Complete parseQuery(search). search may start with ?. Split on &, and for each part split on the first = only. A key with no = gets ''. When a key repeats, the last one wins. Return {} for empty input.

  4. 4

    Answer with a status that means somethingrespond()

    Complete respond(result). result is { ok, value } or { ok: false, error }. Return { status: 200, body: value } on success. On failure return { status: 404, body: { error } } when error is 'not found', { status: 400, body: { error } } when it is 'bad request', and { status: 500, body: { error } } otherwise.

  5. 5

    Route a whole requestroute()

    Complete route(routes, method, url). Each route is { method, path, handler } and handler(params, query) returns a result for respond. Split url on ?. Use the first route whose path matches and whose method matches. When a path matches but no method does, return { status: 405, body: { error: 'method not allowed' } }. When nothing matches, return a 404 response. Handlers are supplied as { ok, value } results under route.result, so handler is not called directly.

How the grading works

Your function is run against the step's test cases by a JavaScript engine inside the app, on your device. Passing is a matter of the tests passing — no model reads your code, no answer is graded by judgement, and nothing you write leaves the phone. Every step's tests are in the same public content bundle the rest of this site is built from.