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:
- http://localhost:8080/ - Static frontend
- http://localhost:8080/swagger-ui - API documentation
- http://localhost:8080/openapi.json - OpenAPI spec
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/apiprefixtk.logger(.{}, ...)wraps routes with logging middleware.router(api)automatically creates routes from all public functions in theapimodule using the@"METHOD /path"naming convention
Next Steps
- See todos_orm_sqlite for database persistence
- Check out the Middlewares guide for more middleware options