Skip to content

How much code does it take to build a REST API with concurrent request handling, in-memory storage, and JSON responses? In Wyn: 93 lines. 69KB binary. Zero dependencies.

The code ​

wyn
items = HashMap.new()
next_id = Shared.new(1)

fn main() -> int {
    server = Http.serve(8080)
    print("API on http://localhost:8080")
    while 1 == 1 {
        req = Http.accept(server)
        method = Http.method(req)
        path = Http.path(req)
        body = Http.req_body(req)
        fd = Http.fd(req)

        if method == "GET" and path == "/api/items" {
            parts = []
            for k in items.keys() { parts.push(items.get(k)) }
            Http.respond(fd, 200, "application/json", "[${parts.join(",")}]")
        } else if method == "POST" and path == "/api/items" {
            id = Shared.add(next_id, 1)
            json = "{\"id\":${id},\"name\":\"${body.trim()}\"}"
            items.set("${id}", json)
            Http.respond(fd, 201, "application/json", json)
        } else {
            Http.respond(fd, 404, "application/json", "{\"error\":\"not found\"}")
        }
    }
    return 0
}

(Simplified from the full 93-line version which includes GET by ID, DELETE, and health check.)

What makes this possible ​

  • Http.serve / Http.accept - built into the stdlib, no framework needed
  • HashMap - built-in key-value store
  • Shared - a counter safe to touch from anywhere
  • "${expr}" - string interpolation builds JSON inline
  • No imports - everything is available out of the box

Why there is no spawn here ​

An earlier version of this post wrapped each request in spawn handle(...) and described it as "each request runs in a green thread". That was a mistake, and it is worth being specific about why, because it is the interesting part.

items is a plain HashMap, and a plain HashMap is not thread-safe. Two handlers writing it at once lose updates or corrupt it, depending on timing - so the old version usually worked, which is the dangerous kind of wrong. As of v1.21 the runtime says so out loud: concurrent mutation panics with concurrent HashMap mutation detected - use a channel or Shared to coordinate writers. Arrays and shared scalar globals already behaved that way; collections were the last gap, and it is closed.

So this version keeps all state on one thread. For an in-memory demo that is not a real cost: the loop is doing nothing but parse-and-respond, and it happily serves 40 concurrent clients on a laptop. When you outgrow it, the honest options are to send changes over a channel to one owning task, or to keep the shared state in something built for it - a database, or Shared for a counter, which is exactly what next_id already is.

The general rule, and it is the one thing to take away: decide where your mutable state lives before you add concurrency. Wyn will now tell you if you get it wrong, but it is a design decision, not a language feature.

Try it ​

bash
wyn new myapi --api
cd myapi
wyn run

The --api template generates a full CRUD API with SQLite persistence.

Comparison ​

WynGoNode.js
Lines93~120~80
Binary69KB6.2MBN/A (50MB runtime)
Dependencies01 (net/http)1+ (express)
Concurrencyspawn (thread pool)goroutinesasync/await

MIT License - v1.22.0