An HTTP Server in Zig

This Zig program implements a minimal single-threaded HTTP server using the standard library’s networking and HTTP facilities. It begins by importing the standard library with const std = @import("std");.

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const address = try std.Io.net.IpAddress.parseIp4(
        "127.0.0.1",
        8080,
    );
    var server = try address.listen(io, .{});
    defer server.deinit(io);

    while (true) {
        const stream = try server.accept(io);
        handleConnection(io, stream) catch |err| {
            std.log.err("connection error: {}", .{err});
        };
    }
}

fn handleConnection(io: std.Io, stream: std.Io.net.Stream) !void {
    defer stream.close(io);
    var reader_buf: [1024]u8 = undefined;
    var writer_buf: [1024]u8 = undefined;
    var reader_impl = stream.reader(io, &reader_buf);
    var writer_impl = stream.writer(io, &writer_buf);
    var http_server = std.http.Server.init(
        &reader_impl.interface,
        &writer_impl.interface,
    );
    var req = try http_server.receiveHead();
    try req.respond("Hello world\n", .{});
}

As expected, the entry point of the program is main(), which receives a std.process.Init value and extracts the I/O handle from it. An IPv4 address for 127.0.0.1 on port 8080 is then parsed with std.Io.net.IpAddress.parseIp4(). That address is used to create a listening server socket via listen(), and the server is cleaned up automatically with a defer call to deinit() when the program exits.

An infinite loop repeatedly accepts incoming connections with server.accept(). Each accepted stream is passed to handleConnection(), and any error that occurs while serving the connection is logged without stopping the server.

Inside handleConnection() the stream is closed automatically via another defer. Fixed-size buffers are allocated for reading and writing, after which reader and writer implementations are obtained from the stream. These are handed to std.http.Server.init() to create an HTTP server instance. The server then receives the request headers with receiveHead() and replies with a simple “Hello world” response via respond().

Being single threaded, the overall design is sequential: each connection is fully handled before the next one is accepted, making the example easy to follow while still demonstrating the core steps of binding, accepting and serving HTTP traffic in Zig.

Save it as minimalHTTP.zig and execute it:

$ zig run ~/Desktop/zigSP/code/ch06/minimalHTTP.zig

Then, from another terminal execute the following command:

$ curl http://127.0.0.1:8080/
Hello world

The output confirms the server is working correctly. The response body matches exactly what handleConnection() sends with req.respond("Hello world\n", .{}).

Happy coding in Zig!

Want to learn Zig? Look at my book Systems Programming with Zig.