Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SQLite & Embedded SPA

This recipe demonstrates building a full-stack CRUD web application with an embedded dark-mode Single Page Application (SPA), REST API endpoints, and a local SQLite database using state injection (Handler.bind).

Source code: examples/todo/


Key Highlights

1. State Injection with Handler.bind

Instead of relying on global database pointers, the App struct holds the database handle and binds methods cleanly to the router:

pub const App = struct {
    allocator: std.mem.Allocator,
    db: *SqliteDB,

    pub fn handleList(self: *App, w: *http.ResponseWriter, req: *http.Request) anyerror!void {
        const todos = try self.db.list(req.arena);
        try w.header().set("Content-Type", "application/json");
        try w.writer().print("{f}\n", .{std.json.fmt(todos, .{})});
    }

    pub fn handleCreate(self: *App, w: *http.ResponseWriter, req: *http.Request) anyerror!void {
        const body = try req.readBodyAlloc(64 * 1024);
        const parsed = try std.json.parseFromSlice(
            struct { title: []const u8 },
            req.arena,
            body,
            .{ .ignore_unknown_fields = true },
        );
        defer parsed.deinit();

        const todo = try self.db.create(req.arena, parsed.value.title);
        w.writeHeader(.created);
        try w.header().set("Content-Type", "application/json");
        try w.writer().print("{f}\n", .{std.json.fmt(todo, .{})});
    }
};

// Route binding in main:
try mux.handle("GET /api/todos", http.Handler.bind(&app, App.handleList));
try mux.handle("POST /api/todos", http.Handler.bind(&app, App.handleCreate));
try mux.handle("POST /api/todos/{id}/toggle", http.Handler.bind(&app, App.handleToggle));
try mux.handle("DELETE /api/todos/{id}", http.Handler.bind(&app, App.handleDelete));

2. Single-Binary Distribution with @embedFile

The frontend HTML/JS/CSS assets are embedded directly into the compiled executable at compile time:

pub fn handleIndex(_: *App, w: *http.ResponseWriter, _: *http.Request) anyerror!void {
    const html = @embedFile("index.html");
    try w.header().set("Content-Type", "text/html; charset=utf-8");
    try w.writeAll(html);
}

The resulting binary has zero filesystem asset dependencies and can be deployed anywhere as a single standalone executable.


Running the Example

make run-todo

Open http://127.0.0.1:7000 in your web browser to interact with the SPA.