Building a Web Framework in C: Linus Server + Custom DSL Compiler
Published on January 23, 2025 · Systems Programming, C, HTTP, DSL, Compilers
Most web frameworks are built on top of other frameworks. Express sits on Node's http module. Laravel sits on PHP's runtime. I wanted to go further down — build a server that speaks HTTP from raw POSIX sockets, has its own routing syntax parsed by a hand-written scanner, and scaffolds new projects through a compiled CLI tool. No runtime. No interpreter. Just C, file descriptors, and fork().
This is the story of Linus — two linked repos: linus_framework (the server) and linus_compiler (the scaffolding tool and DSL parser).
The Server — Raw Sockets, No Dependencies
The core of linus_framework is a TCP server built with getaddrinfo(), bind(), listen(), and accept() — the raw Berkeley socket API. No libevent, no libuv, nothing in between. The main loop accepts connections in a round-robin slot array and spawns a child process per connection via fork():
The respond() function reads the raw HTTP request into a buffer, uses strtok() to split the request line on spaces, checks for GET, validates the HTTP version string, and serves files directly from disk using read() + write() onto the socket file descriptor. There is no JSON parser, no middleware stack, no event loop. The request lifecycle is three system calls.
The Routing DSL — .hk Files
Rather than define routes in C code directly, Linus uses a small custom file format — .hk files. The application descriptor (main.hk) declares the app name and the paths for routes, controllers, and storage:
Route definitions live in r.hk files inside the route/ directory. The syntax is a custom DSL:
That single line encodes four fields: HTTP method (GET), path (/users), controller name (UserController), and handler function (index). The Route.c parser reads this character by character, using state flags toggled by the delimiter characters :, @, and ;:
This is essentially a four-state finite automaton, advancing state on each delimiter character. No regex, no sscanf, no string splitting — just a single fgetc loop. It's the kind of parser you write when you understand what a lexer actually is, not just how to use one.
The Compiler / Scaffolding Tool
linus_compiler is the CLI scaffolding tool linked to the framework. When you run it with a project name, it creates the project directory structure and writes a skeleton r.c file with a default route already in place — the equivalent of rails new, but compiled to a native binary from C.
The server side is wired to invoke the compiled compiler binary via popen(), feeding it route file paths and reading back the output — a pipeline that connects the runtime server to the build tool at the process level.
Why C for a Web Framework?
The standard answer for web frameworks is "because Python/Ruby/Go is expressive enough." But that abstraction has a cost — you never actually understand what happens when a browser makes a connection. Writing Linus forced me to confront every layer: the TCP three-way handshake landing in the kernel's accept queue, the recv() syscall blocking until bytes arrive, strtok being destructive (it modifies the buffer in place, which matters when you're juggling multiple request fields), and why fork() gives you per-request isolation without threads.
The DSL parser taught me something more specific: a hand-written character scanner with explicit state flags is not primitive — it's exactly how the first pass of every real compiler works. The Linus route parser is structurally identical to the tokenizer phase of GCC or Clang, just with a different token grammar.
73, Sunil