blog

A REST API example with Swagger UI documentation and static file serving.

Source Code

Path: examples/blog/

const std = @import("std");
const tk = @import("tokamak");
const model = @import("model.zig");
const api = @import("api.zig");

const App = struct {
    blog_service: model.BlogService,
    server: tk.Server,
    routes: []const tk.Route = &.{
        tk.logger(.{}, &.{
            tk.static.dir("public", .{}),
            .group("/api", &.{.router(api)}),
            .get("/openapi.json", tk.swagger.json(.{ .info = .{ .title = "Example" } })),
            .get("/swagger-ui", tk.swagger.ui(.{ .url = "openapi.json" })),
        }),
    },
};

pub fn main() !void {
    try tk.app.run(tk.Server.start, &.{App});
}

Features Demonstrated

  • REST API with CRUD operations
  • Service layer architecture (BlogService)
  • OpenAPI/Swagger integration
  • Static file serving
  • Route grouping and middleware
  • Logger middleware

Architecture

Main Application

const App = struct {
    blog_service: model.BlogService,
    server: tk.Server,
    routes: []const tk.Route = &.{
        tk.logger(.{}, &.{
            tk.static.dir("public", .{}),
            .group("/api", &.{.router(api)}),
            .get("/openapi.json", tk.swagger.json(.{ .info = .{ .title = "Example" } })),
            .get("/swagger-ui", tk.swagger.ui(.{ .url = "openapi.json" })),
        }),
    },
};

Service Layer

The BlogService manages blog posts in memory:

pub const BlogService = struct {
    posts: std.AutoArrayHashMap(u32, Post),
    next: std.atomic.Value(u32) = .init(1),

    pub fn init(allocator: std.mem.Allocator) !BlogService {
        var posts = std.AutoArrayHashMap(u32, Post).init(allocator);
        try posts.put(1, .{ .id = 1, .title = "Hello, World!", .body = "This is a test post." });
        try posts.put(2, .{ .id = 2, .title = "Goodbye, World!", .body = "This is another test post." });

        return .{ .posts = posts };
    }

    pub fn deinit(self: *BlogService) void {
        // Free all duplicated strings in posts
        for (self.posts.values()) |post| {
            self.posts.allocator.free(post.title);
            self.posts.allocator.free(post.body);
        }
        self.posts.deinit();
    }

    pub fn getPosts(self: *BlogService, allocator: std.mem.Allocator) ![]const Post {
        var res = std.ArrayList(Post){};
        errdefer res.deinit(allocator);

        for (self.posts.values()) |post| {
            try res.append(allocator, try dupe(allocator, post));
        }

        return res.toOwnedSlice(allocator);
    }

    pub fn createPost(self: *BlogService, data: Post) !u32 {
        const post = try dupe(self.posts.allocator, .{
            .id = self.next.fetchAdd(1, .monotonic),
            .title = data.title,
            .body = data.body,
        });
        try self.posts.put(post.id, post);
        return post.id;
    }

    pub fn getPost(self: *BlogService, allocator: std.mem.Allocator, id: u32) !Post {
        return dupe(allocator, self.posts.get(id) orelse return error.NotFound);
    }

    pub fn updatePost(self: *BlogService, id: u32, data: Post) !void {
        try self.posts.put(id, try dupe(self.posts.allocator, data));
    }

    pub fn deletePost(self: *BlogService, id: u32) !void {
        _ = self.posts.orderedRemove(id);
    }

    fn dupe(allocator: std.mem.Allocator, post: Post) !Post {
        return .{
            .id = post.id,
            .title = try allocator.dupe(u8, post.title),
            .body = try allocator.dupe(u8, post.body),
        };
    }
};

API Layer

The API layer (api.zig) defines route handlers that delegate to the service:

pub fn @"GET /posts"(svc: *BlogService, allocator: std.mem.Allocator) ![]const Post {
    return svc.getPosts(allocator);
}

pub fn @"POST /posts"(svc: *BlogService, data: Post) !u32 {
    return svc.createPost(data);
}

Note the special syntax: function names starting with @"GET /posts" define both the HTTP method and path.

API Endpoints

List All Posts

GET /api/posts

Get Single Post

GET /api/posts/:id

Create Post

POST /api/posts
Content-Type: application/json

{
  "id": 0,
  "title": "My Post",
  "body": "Content here"
}

Update Post

PUT /api/posts/:id
Content-Type: application/json

{
  "id": 1,
  "title": "Updated Title",
  "body": "Updated content"
}

Delete Post

DELETE /api/posts/:id

Swagger UI

Visit http://localhost:8080/swagger-ui for interactive API documentation where you can:

  • Browse all endpoints
  • See request/response schemas
  • Try out the API directly in your browser

Running

cd examples/blog
zig build run

Then open:

Key Patterns

Route Grouping, Middleware, and Router

The main application shows all these patterns together:

    routes: []const tk.Route = &.{
        tk.logger(.{}, &.{
            tk.static.dir("public", .{}),
            .group("/api", &.{.router(api)}),
            .get("/openapi.json", tk.swagger.json(.{ .info = .{ .title = "Example" } })),
            .get("/swagger-ui", tk.swagger.ui(.{ .url = "openapi.json" })),
        }),
    },
  • .group("/api", ...) groups all API routes under /api prefix
  • tk.logger(.{}, ...) wraps routes with logging middleware
  • .router(api) automatically creates routes from all public functions in the api module using the @"METHOD /path" naming convention

Next Steps