todos_orm_sqlite
A complete REST API using SQLite database with ORM integration.
Source Code
Path: examples/todos_orm_sqlite/
const std = @import("std");
const tk = @import("tokamak");
const fr = @import("fridge");
const Status = std.http.Status;
const Todo = struct {
pub const sql_table_name = "todos";
id: ?u32 = null,
title: []const u8,
is_done: bool = false,
};
pub const PatchTodoReq = struct {
title: ?[]const u8 = null,
is_done: ?bool = null,
};
const App = struct {
db_pool: fr.Pool(fr.SQLite3),
db_opts: fr.SQLite3.Options = .{ .filename = ":memory:" },
db_pool_opts: fr.PoolOptions = .{ .max_count = 4 },
server: tk.Server,
server_opts: tk.ServerOptions = .{ .listen = .{ .port = 8080 } },
routes: []const tk.Route = &.{
// add debug logging
tk.logger(.{}, &.{
// provide the db session, group endpoints under /todo
.provide(fr.Pool(fr.SQLite3).getSession, &.{.group("/todo", &.{
.get("/", readAll),
.get("/:id", readOne),
.post("/", create),
.put("/:id", update),
.patch("/:id", patch),
.delete("/:id", delete),
})}),
}),
},
pub fn configure(bundle: *tk.Bundle) void {
// Register some callbacks to be auto-called during the app init.
bundle.addInitHook(initDb);
bundle.addInitHook(printServerPort);
}
fn initDb(allocator: std.mem.Allocator, db_pool: *fr.Pool(fr.SQLite3)) !void {
var db = try db_pool.getSession(allocator);
defer db.deinit();
try db.exec(
\\ CREATE TABLE IF NOT EXISTS todos (
\\ id INTEGER PRIMARY KEY AUTOINCREMENT,
\\ title TEXT NOT NULL,
\\ is_done BOOLEAN NOT NULL
\\ );
, .{});
try db.exec(
\\ INSERT INTO todos (title, is_done) VALUES
\\ ('Learn Zig', 0),
\\ ('Build a Tokamak app', 0),
\\ ('Master SQLite', 1);
, .{});
}
fn printServerPort(server_opts: tk.ServerOptions) void {
std.debug.print("Starting tokamak on: http://localhost:{d}/todo\n", .{server_opts.listen.port});
}
};
pub fn main(init: std.process.Init) !void {
try tk.app.run(init, tk.Server.start, &.{App});
}
fn readOne(db: *fr.Session, id: u32) !Todo {
return try db.query(Todo).where("id", id).findOne() orelse error.NotFound;
}
fn readAll(db: *fr.Session) ![]const Todo {
return try db.query(Todo).findAll();
}
fn create(res: *tk.Response, db: *fr.Session, data: Todo) !Todo {
res.status = @intFromEnum(Status.created);
return try db.query(Todo).insert(data).returning("*").fetchOne(Todo) orelse error.InternalServerError;
}
fn update(db: *fr.Session, id: u32, data: Todo) !void {
return try db.update(Todo, id, data);
}
fn patch(db: *fr.Session, id: u32, data: PatchTodoReq) !void {
var row = try db.query(Todo).where("id", id).findOne() orelse return error.NotFound;
applyPatch(&row, data);
return try db.update(Todo, id, row);
}
fn delete(db: *fr.Session, id: u32) !void {
try db.query(Todo).where("id", id).delete().exec();
}
// helper for updating all fields which are set in the body and not null / undefined
fn applyPatch(dest: anytype, ptch: anytype) void {
inline for (comptime std.meta.fieldNames(@TypeOf(ptch))) |f| {
if (@field(ptch, f)) |v| {
@field(dest, f) = v;
}
}
}
Features Demonstrated
- Database integration with fridge ORM
- Connection pooling
- Database migrations
- CRUD operations with SQL
- PATCH endpoint with partial updates
- Lifecycle hooks (
configure,initDb) - Custom middleware composition
Model
const Todo = struct {
pub const sql_table_name = "todos";
id: ?u32 = null,
title: []const u8,
is_done: bool = false,
};
Database Setup
The app uses lifecycle hooks to initialize the database:
const App = struct {
db_pool: fr.Pool(fr.SQLite3),
db_opts: fr.SQLite3.Options = .{ .filename = ":memory:" },
db_pool_opts: fr.PoolOptions = .{ .max_count = 4 },
server: tk.Server,
server_opts: tk.ServerOptions = .{ .listen = .{ .port = 8080 } },
routes: []const tk.Route = &.{
// add debug logging
tk.logger(.{}, &.{
// provide the db session, group endpoints under /todo
.provide(fr.Pool(fr.SQLite3).getSession, &.{.group("/todo", &.{
.get("/", readAll),
.get("/:id", readOne),
.post("/", create),
.put("/:id", update),
.patch("/:id", patch),
.delete("/:id", delete),
})}),
}),
},
pub fn configure(bundle: *tk.Bundle) void {
// Register some callbacks to be auto-called during the app init.
bundle.addInitHook(initDb);
bundle.addInitHook(printServerPort);
}
fn initDb(allocator: std.mem.Allocator, db_pool: *fr.Pool(fr.SQLite3)) !void {
var db = try db_pool.getSession(allocator);
defer db.deinit();
try db.exec(
\\ CREATE TABLE IF NOT EXISTS todos (
\\ id INTEGER PRIMARY KEY AUTOINCREMENT,
\\ title TEXT NOT NULL,
\\ is_done BOOLEAN NOT NULL
\\ );
, .{});
try db.exec(
\\ INSERT INTO todos (title, is_done) VALUES
\\ ('Learn Zig', 0),
\\ ('Build a Tokamak app', 0),
\\ ('Master SQLite', 1);
API Endpoints
Create Todo
curl -X POST -H "content-type: application/json" \
-d '{ "title": "my todo" }' \
http://localhost:8080/todo
Status: 201 Created
{ "id": 1, "title": "my todo", "is_done": false }
List All Todos
curl http://localhost:8080/todo
Status: 200 OK
[{ "id": 1, "title": "my todo", "is_done": false }]
Get Single Todo
curl http://localhost:8080/todo/1
Status: 200 OK
{ "id": 1, "title": "my todo", "is_done": false }
Update Todo (Full Replacement)
curl -X PUT -H "content-type: application/json" \
-d '{ "id": 1, "is_done": true, "title": "my todo" }' \
http://localhost:8080/todo/1
Status: 204 No Content
Patch Todo (Partial Update)
curl -X PATCH -H "content-type: application/json" \
-d '{ "title": "new title only" }' \
http://localhost:8080/todo/1
Status: 204 No Content
Only the fields provided are updated. This example includes a helper function patchSetFields that handles partial updates generically.
Delete Todo
curl -X DELETE http://localhost:8080/todo/1
Status: 204 No Content
Handler Examples
Create with Custom Status
return try db.query(Todo).where("id", id).findOne() orelse error.NotFound;
}
fn readAll(db: *fr.Session) ![]const Todo {
Read with ORM
}
};
pub fn main(init: std.process.Init) !void {
try tk.app.run(init, tk.Server.start, &.{App});
}
Delete with Query Builder
return try db.update(Todo, id, data);
}
Configuration
Database and connection pool options are configured in the App struct:
db_pool: fr.Pool(fr.SQLite3),
db_opts: fr.SQLite3.Options = .{ .filename = ":memory:" },
db_pool_opts: fr.PoolOptions = .{ .max_count = 4 },
- Change
filenameto a path like"db.sqlite"for persistence - Adjust
max_countfor connection pool size
Running
cd examples/todos_orm_sqlite
zig build run
The server starts at http://localhost:8080/todo
Dependency Injection Pattern
Database sessions are provided to handlers via middleware:
routes: []const tk.Route = &.{
// add debug logging
tk.logger(.{}, &.{
// provide the db session, group endpoints under /todo
.provide(fr.Pool(fr.SQLite3).getSession, &.{.group("/todo", &.{
.get("/", readAll),
.get("/:id", readOne),
.post("/", create),
.put("/:id", update),
.patch("/:id", patch),
.delete("/:id", delete),
})}),
}),
},
Handlers simply request db: *fr.Session and get an active session.
Next Steps
- See the fridge ORM documentation for more query capabilities
- Check out blog for service layer patterns
- Read the Dependency Injection guide for more on DI patterns