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

12 April 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

A Prompt for Smoother Claude Code Onboarding

A Prompt for Smoother Claude Code Onboarding

Onboarding new AI tools often stalls on unclear rules, scattered files, and lengthy reviews. Discover a concise Claude Code prompt and step-by-step workflow that auto-generates, updates, and proposes missing docs.

23 June 2025

How to Use Circle API to Trade USDC

How to Use Circle API to Trade USDC

USD Coin (USDC) has emerged as a cornerstone of stability and reliability. As a fully reserved, dollar-backed stablecoin, USDC bridges the gap between traditional fiat currency and the burgeoning world of digital assets. It offers the speed and global reach of cryptocurrencies while maintaining the price stability of the U.S. dollar, making it an ideal medium for commerce, trading, and remittances on the internet. At the heart of the USDC ecosystem is Circle, the principal developer of the stab

23 June 2025

Cursor Is Down? Cursor Shows Service Unavailable Error? Try These:

Cursor Is Down? Cursor Shows Service Unavailable Error? Try These:

This guide will walk you through a series of troubleshooting steps, from the simplest of checks to more advanced solutions, to get you back to coding.

22 June 2025

Practice API Design-first in Apidog

Discover an easier way to build and use APIs