Skip to content

Šta za ručak?

Zig · Next.js · Postgres

A website for Serbian home-cooking recipes. I wrote the backend in Zig, along with the web framework and HTTP library it runs on.

Live site Source is private.

Why I built it

I had been building backends on top of frameworks for a while, and I wanted to know what they were doing for me. So I wrote the HTTP layer myself, then a small framework on top of it, and then I needed something real to run on them.

A recipe site was a good fit. It has accounts, sessions, search and moderation, which covers most of what an ordinary backend does.

What it runs on

Caddy sits in front and serves the static files. Next.js renders the pages. The API is Zig and talks to Postgres. The Zig part is three layers, and I wrote all of them.

martensite, the HTTP library

martensite is HTTP/1.1 and nothing else. It parses requests and responses, works out where a body ends, handles chunked encoding and writes responses. It has no router and no middleware. Nothing in it allocates. Every slice it gives you points into a buffer you own.

martensite rejects any request that could be read two ways, and it never writes a message its own parser would reject. When a proxy and a server disagree about where a body ends, someone can hide a second request inside the first one, so both sides have to follow the same rules.

I test the parser against picohttpparser, which has been in production for years. If picohttpparser rejects a request, martensite has to reject it too. The other way round is allowed.

zither, the web framework

zither adds routing, a context for each request and the accept loop. The route table is built at compile time, so two routes that would match the same request are a compile error. Each connection gets an arena that is reset before the next request, so a handler allocates freely and never frees anything.

The app

The API handles accounts, recipes, search and moderation. Sessions are an httpOnly cookie with the session id. There are no JWTs.

Why Zig

I wanted a language that doesn't do anything I didn't ask for. Zig has no garbage collector, no hidden allocations and no async runtime picking threads for me. Anything that needs memory takes an allocator as an argument, so I had to decide where every buffer lives and how long it stays valid.

A framework usually handles that for you, and I wanted to learn it. I already knew Rust, but I'd have reached for hyper and tokio and never written the parser myself.