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

Tyk vs Kong: Which API Gateway Should You Choose in 2025?

Tyk vs Kong: Which API Gateway Should You Choose in 2025?

Compare Tyk vs Kong in this comprehensive guide. Discover their features, pros & cons and real-world use cases. Learn how Apidog complements them for complete API lifecycle management.

8 August 2025

Docs as Code: The Ultimate Guide to Modern API Documentation That Scales

Docs as Code: The Ultimate Guide to Modern API Documentation That Scales

Learn how Docs as Code methodology combined with Apidog's AI-powered features creates scalable, accurate API documentation that evolves with your code. From visual API design to LLMs.txt and MCP Server integration, discover the future of technical documentation.

8 August 2025

GPT-5 vs Claude Opus: Comparing their API Pricing, and Which is Better for Coding

GPT-5 vs Claude Opus: Comparing their API Pricing, and Which is Better for Coding

Dive into GPT-5 vs Claude Opus 4.1! Compare coding benchmarks, API pricing, and features to find the best AI for your 2025 projects. Perfect for developers seeking speed or precision.

8 August 2025

Practice API Design-first in Apidog

Discover an easier way to build and use APIs