A clone-and-go starter for building HTTP services in Zig on the wing web framework. Fork it, rename it, and start writing features — the build wiring, server bootstrap, auth, OpenAPI generation, and request tracing are already in place.
Stack:
zio(coroutine runtime) →talon(HTTP engine) →wing(web framework, ≈ ASP.NET Core / axum), withmantleas the pure-Zig MySQL driver. Routing, extractors,Context, and middleware are resolved at comptime, so the steady-state request path does no reflection and no heap allocation.
- Handlers are just typed functions. Declare what you need as parameters —
wing.Path(T),wing.Json(T),*UserService,auth: Auth— and wing wires it at comptime. No annotations, no DI container; shared state is one explicitAppStatestruct projected to handlers by type. - Feature modules, not a controller pile. Each feature is a vertical slice
(
handlers/ routes/ services/ repositories/ models/) with one file per endpoint. The app grows by adding feature folders, not by growing one giant table or controller. - Auth that can't drift from its docs. Cookie sessions and bearer API tokens resolve through a
single argon2 hashed-secret credential store, with role-based authorization. The accepted
channels live in one file (
auth/support/auth.zig); both the runtime checks and the OpenAPIsecuritySchemesderive from it. - OpenAPI 3.1, generated from your code. The spec is assembled at comptime from the router and
handler signatures — served live at
/openapi.json, rendered by a Scalar UI at/docs, and dumpable offline withzig build openapi. - Request tracing built in. Every request gets a time-sortable UUIDv7 (26-char Crockford
Base32) on the
x-request-idheader, and every log line it produces — app, wing, and mantle SQL — carries that id. - Production-minded defaults. Graceful
SIGINTdrain, fd-limit-aware connection admission, and a single-executor zio runtime tuned for syscall-bound work against a co-located database.
-
Zig 0.16.0 (
minimum_zig_versionis pinned inbuild.zig.zon). -
Local checkouts of
wingandmantleas sibling directories (../wing,../mantle). This starter depends on them by path while they are pre-release; see Dependencies to switch to pinned releases later. -
A reachable MySQL server with the target database already created. The app manages its own tables, not the database:
mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS wing_app;"Connection settings come from
DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME/DB_POOL_SIZE(defaults:127.0.0.1:3306,root/root, dbwing_app, pool8); the listen port isPORT(default8080). Tables are created from embedded SQL migrations at startup.
git clone <this-repo> my-app && cd my-app
zig build run
# in another terminal:
curl http://127.0.0.1:8080/ # -> HTML greeting
curl http://127.0.0.1:8080/health # -> health check
open http://127.0.0.1:8080/docs # -> interactive API docs (Scalar)Ctrl+C triggers a graceful drain (the server watches SIGINT and shuts down cleanly).
Other build steps:
zig build # compile to zig-out/bin/wing_app
zig build test # run unit tests (auth + openapi)
zig build openapi # print the assembled OpenAPI 3.1 spec to stdoutThe users and auth features are threaded through every layer as worked examples:
| Method & path | Purpose |
|---|---|
GET / |
HTML home page (view layer) |
GET /health |
Health check |
GET /docs, /openapi.json |
Scalar UI + generated OpenAPI spec |
GET /api/v1/users |
List users |
POST /api/v1/users |
Create user |
GET /api/v1/users/:id |
Get user by id |
POST /api/v1/auth/login |
Log in (issues a session cookie) |
POST /api/v1/auth/logout |
Revoke the session |
POST /api/v1/auth/token/issue |
Issue a bearer API token |
POST /api/v1/auth/token/revoke |
Revoke an API token |
GET /api/v1/auth/me |
Current authenticated user (cookie/bearer) |
A feature-module structure: shared infrastructure at the root of src/, and one self-contained
folder per business feature. Each feature depends on the shared layers, not on its siblings.
.
├── build.zig # build graph: wing + transitive talon/zio, plus mantle
├── build.zig.zon # manifest: dependency pins, Zig version floor
└── src/
├── main.zig # entry point — hands the allocator to the server
├── server.zig # bootstrap: runtime, listener, HTTP server, graceful drain
├── app.zig # middleware chain + error→status mapping
├── state.zig # AppState: the explicit dependency graph (no DI container)
├── config/ # config: compiled defaults + env overrides
├── db/ # MySQL pool + embedded SQL migrations (migrations/*.sql)
├── trace/ # request-id scope threaded through every log line
├── middleware/ # request_scope (req id), security_headers
├── openapi/ # comptime OpenAPI 3.1 generator (wraps wing.Router)
├── routes/ # top-level URL→feature composition (nest / merge / fallback)
├── handlers/ # root ops endpoints (home, health)
├── views/ # server-rendered output (HTML)
├── user/ # feature module ──┐
│ ├── handlers/ # one file per endpoint (index / show / create)
│ ├── routes/ # feature sub-router (relative paths, nested by routes.zig)
│ ├── services/ # business rules, HTTP-agnostic
│ ├── repositories/ # data access on the shared pool
│ └── models/ # entities + request/response DTOs
├── auth/ # feature module: login, tokens, sessions, roles
│ ├── handlers/ # login / logout / me / token_issue / token_revoke
│ ├── services/ # auth + credential store (argon2 hashed secrets)
│ ├── repositories/ # credentials + roles
│ ├── support/ # auth policy (schemes/locators), authorizer, password
│ └── models/ # principal
└── docs/ # feature module: serves /openapi.json + Scalar /docs
Request flow within a feature: routes → handler → service → repository, with models
shared and views rendering output. A handler stays thin — extract, delegate, respond:
// src/user/handlers/show.zig — GET /api/v1/users/:id
pub fn handle(ctx: *Ctx, svc: *UserService, path: wing.Path(struct { id: u64 })) anyerror!wing.Json(User) {
const user = try svc.get(ctx.arena, path.value.id);
return .{ .value = user };
}svc: *UserService is projected from AppState by type; wing.Path parses the route param;
error.NotFound from the service maps to 404 for free. Map any custom domain errors to status
codes once, in app.zig.
Create a folder under src/<feature>/ and fill the slice bottom-up:
- Model (
models/) — the entity and its request/response DTOs. - Repository (
repositories/) — data access: lease a pooled connection, run parameterized SQL. - Service (
services/) — business rules; HTTP-agnostic, returns domain types or errors. - Handler (
handlers/) — one file per endpoint: extract withwing.Path/Query/Json, require auth by declaringauth: Auth, return a typed response (wing.Json(T),wing.Created(T), …). - Routes (
routes/) — a sub-router mapping paths to handlers;nestit insrc/routes/routes.zig. - State (
state.zig) — add the service as a top-level field so handlers can project it.
To protect an endpoint, add auth: Auth (cookie-or-bearer) or auth: RequireRole("admin") to its
handler signature — that is the compile-time proof the route needs auth, and the OpenAPI security
block updates automatically. See src/auth/support/auth.zig for the policy and per-route overrides
(CookieOnly / BearerOnly).
For framework details (extractors, typed responses, state projection, custom middleware, error
handling), see wing's user guide at ../wing/docs/user-guide.md. For the OpenAPI generator, see
lib/wing-openapi/docs/openapi-user-guide.md.
Declared in build.zig.zon as local paths during pre-release co-development:
.dependencies = .{
.wing = .{ .path = "../wing" }, // web framework
.mantle = .{ .path = "../mantle" }, // pure-Zig MySQL driver
},build.zig reaches through wing's dependency graph to also expose talon (TcpListener /
Server) and zio (the Runtime) to the executable — and because mantle depends on the same
../zio, Zig dedupes to a single zio instance, so the server runtime and the DB pool share one
scheduler. When wing/talon/mantle publish tagged releases, replace each .path with a url + hash.
- Rename the package:
.nameinbuild.zig.zonand.nameinbuild.zig(wing_app→ your app). - Regenerate
.fingerprintinbuild.zig.zon(zig buildprompts if it's stale). - Replace or delete the
users/authexample features and updatestate.zig+routes/routes.zig. - Adjust
config/config.zig, the middleware chain inapp.zig, and the home view to taste.
This is pre-release software co-developed with wing, talon, and mantle, so the API surface and defaults may still shift. It already runs a real MySQL-backed service with auth and generated docs — kicking the tires and reporting friction is genuinely useful. Open an issue with what you tried, what you expected, and what happened; bug reports, rough edges, and "this was confusing" notes are all welcome.