How to Use Zig to Build REST API

In this tutorial, we'll explore how to leverage Zig to create a REST API that's not just functional but "blazingly fast.

Mark Ponomarev

Mark Ponomarev

26 June 2025

How to Use Zig to Build REST API

Zig is a modern programming language designed to be robust, optimal, and maintainable. With its focus on performance, explicit control over memory allocation, and compile-time features, Zig is an excellent choice for building high-performance applications, including REST APIs that need to handle significant loads with minimal resource usage.

In this tutorial, we'll explore how to leverage Zig to create a REST API that's not just functional but "blazingly fast." We'll walk through setting up a project, implementing the core API functionality, and optimizing it for peak performance. By the end, you'll have a solid foundation for building high-performance web services using Zig.

💡
While many developers are familiar with Postman as the API Testing tool, I'd like to introduce you to Apidog, a powerful alternative that offers a complete API development platform.

Apidog combines API documentation, design, testing, and debugging into one seamless workflow, making it perfect for testing our high-performance Zig API.

button

Setting Up Your Zig Development Environment

Before we start coding, let's ensure you have everything needed to develop with Zig:

Install Zig: Download the latest version from ziglang.org or use a package manager:

# On macOS with Homebrew
brew install zig

# On Linux with apt
sudo apt install zig

Verify installation:

zig version

Project Structure

Let's create a clean project structure:

mkdir zig-rest-api
cd zig-rest-api
zig init-exe

This creates a basic Zig project with a src/main.zig file. We'll expand this structure:

zig-rest-api/
├── src/
│   ├── main.zig
│   ├── server.zig
│   ├── router.zig
│   ├── handlers/
│   │   ├── users.zig
│   │   └── products.zig
│   └── models/
│       ├── user.zig
│       └── product.zig
├── build.zig
└── README.md

HTTP Server Implementation

First, let's create a basic HTTP server. Zig's standard library provides networking primitives, but it doesn't include a full HTTP server. We'll use the excellent zhttp package.

Add this dependency to your build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const zhttp = b.dependency("zhttp", .{});
    
    const exe = b.addExecutable(.{
        .name = "zig-rest-api",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });
    
    exe.addModule("zhttp", zhttp.module("zhttp"));
    
    b.installArtifact(exe);
}

Now let's implement our server in src/server.zig:

const std = @import("std");
const zhttp = @import("zhttp");
const Allocator = std.mem.Allocator;

pub const Server = struct {
    server: zhttp.Server,
    allocator: Allocator,

    pub fn init(allocator: Allocator, port: u16) !Server {
        return Server{
            .server = try zhttp.Server.init(allocator, .{
                .address = "0.0.0.0",
                .port = port,
            }),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *Server) void {
        self.server.deinit();
    }

    pub fn start(self: *Server) !void {
        std.log.info("Server listening on port {d}", .{self.server.port});
        try self.server.start();
    }
};

Adding Router and Request Handling

In src/router.zig, let's create a simple router:

const std = @import("std");
const zhttp = @import("zhttp");
const Allocator = std.mem.Allocator;

pub const Router = struct {
    routes: std.StringHashMap(HandlerFn),
    allocator: Allocator,

    pub const HandlerFn = fn(zhttp.Request, *zhttp.Response) anyerror!void;

    pub fn init(allocator: Allocator) Router {
        return Router{
            .routes = std.StringHashMap(HandlerFn).init(allocator),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *Router) void {
        self.routes.deinit();
    }

    pub fn addRoute(self: *Router, path: []const u8, handler: HandlerFn) !void {
        try self.routes.put(try self.allocator.dupe(u8, path), handler);
    }

    pub fn handleRequest(self: *Router, req: zhttp.Request, res: *zhttp.Response) !void {
        const handler = self.routes.get(req.path) orelse {
            res.status = .not_found;
            try res.send("Not Found");
            return;
        };

        try handler(req, res);
    }
};

JSON Handling

Let's create utility functions for working with JSON:

// src/json_utils.zig
const std = @import("std");

pub fn parseJson(comptime T: type, json_str: []const u8, allocator: std.mem.Allocator) !T {
    var tokens = std.json.TokenStream.init(json_str);
    return try std.json.parse(T, &tokens, .{
        .allocator = allocator,
        .ignore_unknown_fields = true,
    });
}

pub fn stringify(value: anytype, allocator: std.mem.Allocator) ![]const u8 {
    return std.json.stringifyAlloc(allocator, value, .{});
}

Building API Handlers

Now let's create a handler for user resources in src/handlers/users.zig:

const std = @import("std");
const zhttp = @import("zhttp");
const json_utils = @import("../json_utils.zig");

const User = struct {
    id: usize,
    name: []const u8,
    email: []const u8,
};

// In-memory store for demonstration
var users = std.ArrayList(User).init(std.heap.page_allocator);

pub fn getUsers(req: zhttp.Request, res: *zhttp.Response) !void {
    const json = try json_utils.stringify(users.items, req.allocator);
    res.headers.put("Content-Type", "application/json") catch unreachable;
    try res.send(json);
}

pub fn getUserById(req: zhttp.Request, res: *zhttp.Response) !void {
    // Extract ID from URL path parameters
    const id_str = req.params.get("id") orelse {
        res.status = .bad_request;
        try res.send("Missing ID parameter");
        return;
    };
    
    const id = try std.fmt.parseInt(usize, id_str, 10);
    
    // Find user with matching ID
    for (users.items) |user| {
        if (user.id == id) {
            const json = try json_utils.stringify(user, req.allocator);
            res.headers.put("Content-Type", "application/json") catch unreachable;
            try res.send(json);
            return;
        }
    }
    
    res.status = .not_found;
    try res.send("User not found");
}

pub fn createUser(req: zhttp.Request, res: *zhttp.Response) !void {
    const body = try req.body();
    var user = try json_utils.parseJson(User, body, req.allocator);
    
    // Generate a new ID
    user.id = users.items.len + 1;
    
    try users.append(user);
    
    res.status = .created;
    res.headers.put("Content-Type", "application/json") catch unreachable;
    const json = try json_utils.stringify(user, req.allocator);
    try res.send(json);
}

Putting It All Together

Now, let's update our src/main.zig to integrate everything:

const std = @import("std");
const Server = @import("server.zig").Server;
const Router = @import("router.zig").Router;
const users = @import("handlers/users.zig");

pub fn main() !void {
    // Create an arena allocator
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    var router = Router.init(allocator);
    defer router.deinit();

    // Register routes
    try router.addRoute("/api/users", users.getUsers);
    try router.addRoute("/api/users/{id}", users.getUserById);
    try router.addRoute("/api/users", users.createUser);

    var server = try Server.init(allocator, 8080);
    defer server.deinit();

    // Set up the request handler
    server.server.setRequestHandler(handler);

    // Start the server
    try server.start();
}

fn handler(req: zhttp.Request, res: *zhttp.Response) !void {
    std.log.info("{s} {s}", .{@tagName(req.method), req.path});
    try router.handleRequest(req, res);
}

Performance Optimizations

Zig provides several features that make our API blazingly fast:

  1. Zero-cost abstractions: Zig's approach ensures that abstractions don't add overhead.
  2. Compile-time metaprogramming: We can perform work at compile time instead of runtime.
  3. Manual memory management: By controlling allocations, we avoid garbage collection pauses.
  4. Arena allocators: Perfect for request-scoped memory that can be quickly freed all at once.
  5. Inline functions: Critical paths can be inlined to eliminate function call overhead.

Let's optimize our router:

// Add inline attribute to critical functions
pub inline fn handleRequest(self: *Router, req: zhttp.Request, res: *zhttp.Response) !void {
    // Existing code...
}

And optimize our request handler with a thread pool:

const ThreadPool = struct {
    threads: []std.Thread,
    
    pub fn init(thread_count: usize) !ThreadPool {
        var threads = try std.heap.page_allocator.alloc(std.Thread, thread_count);
        
        for (threads) |*thread, i| {
            thread.* = try std.Thread.spawn(.{}, workerFn, .{i});
        }
        
        return ThreadPool{ .threads = threads };
    }
    
    fn workerFn(id: usize) void {
        // Process incoming requests
        while (true) {
            // Get request from queue and process
        }
    }
};

Conclusion

In this tutorial, we've explored how to leverage Zig's performance-focused features to build a high-performance REST API. By taking advantage of Zig's manual memory management, compile-time features, and zero-cost abstractions, we've created an API server that's both robust and blazingly fast.

We covered:

This foundation gives you the tools to build production-ready APIs in Zig that outperform those written in many other languages. As you continue developing with Zig, explore more advanced features like comptime, error handling, and cross-compilation to further enhance your applications.

Explore more

Voxtral: Mistral AI's Open Source Whisper Alternative

Voxtral: Mistral AI's Open Source Whisper Alternative

For the past few years, OpenAI's Whisper has reigned as the undisputed champion of open-source speech recognition. It offered a level of accuracy that democratized automatic speech recognition (ASR) for developers, researchers, and hobbyists worldwide. It was a monumental leap forward, but the community has been eagerly awaiting the next step—a model that goes beyond mere transcription into the realm of true understanding. That wait is now over. Mistral AI has entered the ring with Voxtral, a ne

15 July 2025

How to build, deploy and host MCP servers on Netlify

How to build, deploy and host MCP servers on Netlify

Build and deploy MCP servers on Netlify to connect AI agents with your platform. This guide covers setup, deployment, and testing with a sample prompt, making AI workflows a breeze with Netlify’s serverless power.

15 July 2025

How to Use Kimi K2 in Cursor

How to Use Kimi K2 in Cursor

Learn how to use Kimi K2 in Cursor, why developers are demanding this integration, and how Apidog MCP Server lets you connect, document, and automate your API workflows with Kimi K2.

15 July 2025

Practice API Design-first in Apidog

Discover an easier way to build and use APIs