A Zig Web Skeleton That Ships with MySQL, Auth, and OpenAPI — and Hits 60K RPS on an M1
Recommend a Zig Web Project Skeleton: wing-app
Recently, I saw a Zig Web project written by a friend: dacheng-zig/wing-app.
It's not a toy demo that only runs a Hello, world!, but a Web service skeleton you can clone and start modifying business logic on. For those who want to try writing backends in Zig but don't want to build everything from scratch — HTTP listening, routing, middleware, database connections, authentication, API documentation — wing-app is a great starting point.
In one sentence: it combines zio, talon, wing, and mantle from the Zig ecosystem into an engineering-oriented Web application template.
It Doesn't Just Solve "Can You Write Web"
Many early examples in languages or frameworks stop at the "start a service, return a string" stage.
This is certainly important, but when actually writing business services, the questions immediately become:
- How to organize routes?
- How to layer controller, service, repository?
- Where does configuration come from?
- Who holds the database connection pool?
- How to uniformly map business errors to HTTP status codes?
- How to handle login state?
- Can API documentation be generated automatically?
- After the project grows, can the directory structure still be understood?
What's interesting about wing-app is that it doesn't just showcase framework APIs; it directly embeds these engineering problems into the project structure.
Its README clearly states its positioning: this is a minimal, clone-and-go skeleton based on the wing Web framework. That means you can fork or clone it, change the package name, delete the example business logic, and then start writing your own routes, handlers, and state.
Tech Stack: zio + talon + wing + mantle
The current tech stack of wing-app is roughly:
zio: coroutine runtimetalon: HTTP enginewing: Web framework, responsible for routing, middleware, Context, extractors, etc.mantle: pure Zig MySQL driver, used for database access
There's a key description in the README: wing's framework magic is resolved at comptime, with no reflection or heap allocation on the stable request path.
This is very Zig.
The most attractive part of writing Web in Zig isn't just "different syntax," but the ability to move many things that dynamic frameworks do at runtime to compile time. Handler function signatures, request extraction, state projection, response types — all can become structures checkable at compile time.
For example, a user detail endpoint can be written like this:
pub fn show(
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 };
}
Several design orientations can be seen here:
wing.Path(struct { id: u64 })explicitly expresses path parameters*UserServiceis projected fromAppStateby type- The return value is
wing.Json(User), the response type is also explicit - The controller only handles HTTP binding and response organization; business logic is in the service
If you're used to writing Spring, ASP.NET Core, NestJS, or axum, this structure will feel familiar. But behind it are Zig's comptime and explicit dependencies, not a black-box DI container.
The Project Structure is Very "Backend Engineering"
The directory layout of wing-app isn't haphazardly thrown together; it adopts classic layering:
src/
├── main.zig
├── server.zig
├── app.zig
├── state.zig
├── config/
├── db/
├── routes/
├── controllers/
├── services/
├── repositories/
├── models/
├── middleware/
├── views/
├── auth/
├── user/
├── docs/
├── openapi/
└── trace/
Both the README and docs/architecture.md emphasize the request flow:
routes -> controller -> service -> repository
Each layer does only its own job:
routeshandles the composition of URLs to functional modulescontrollershandle request binding, calling services, organizing responsesserviceshandle business rules, unaware of HTTPrepositorieshandle data accessmodelshold entities and DTOsstate.zigassembles config, database, repositories, services together
This structure isn't novel, but it's very practical.
I've always felt that for a Web framework to truly land, it can't just tell you "how to register routes"; it also needs to tell you "where code should go when it grows up." wing-app scores points on this.
No DI Container, But Dependencies Are Clear
wing-app uses AppState to explicitly aggregate application dependencies.
For example, configuration, database, user services, authentication services, session services, etc., are all hung on a unified application state. Whatever a handler needs, it declares a pointer of the corresponding type in the function parameters, and the framework completes the projection by type at compile time.
This approach has two advantages:
First, dependency relationships are explicit. You look at the handler signature and know what it needs.
Second, there's no implicit magic like runtime DI containers. The assembly point is in state.zig, making problems easier to locate.
The cost is also clear: top-level field types need to remain distinguishable. If there are two dependencies of the same type, you need to wrap them in different structs for disambiguation.
I like this trade-off because it fits Zig's temperament: less implicit, more readable structure.
The Database Isn't Fake Data: MySQL is Already Connected
Many backend skeleton projects like to demonstrate CRUD with in-memory arrays first, and when it's time to actually connect a database, it's a whole different story.
wing-app is not like that. It has already connected to MySQL via mantle, with a connection pool, migrations, and a repository layer.
The current project has these migration files:
001_create_users.sql002_create_roles.sql003_create_sessions.sql
The application manages its own table structure on startup, but won't automatically create the database. The README also explains that you need to create the database in advance, for example:
mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS wing_app;"
Database configuration comes from environment variables:
DB_HOSTDB_PORTDB_USERDB_PASSWORDDB_NAMEDB_POOL_SIZE
Defaults are local MySQL, root/root, database wing_app, connection pool size 8.
This shows the author isn't just trying to make a demo that "looks like it runs," but is building the infrastructure a real service would use.
Authentication and Authorization Already Have a Prototype
The project also has a complete auth module, including:
- login
- logout
- me
- session repository
- role repository
- password support
- auth extractor
- authorizer
After login, a server-side session is created, and a session_id is returned via cookie. The cookie has attributes like HttpOnly, Secure, SameSite=Strict.
The interface layer can also express "this endpoint requires login" through types.
For example, the handlers for logout and me declare Auth in their parameters. This isn't a convention enforced by comments, but an authentication requirement expressed through the function signature:
pub fn me(ctx: *Ctx, auth: Auth) anyerror!wing.Json(Profile) {
const user = try ctx.state.users.get(ctx.arena, auth.principal.id);
return .{ .value = .{
.id = user.id,
.name = user.name,
.roles = auth.principal.roles,
} };
}
This kind of design suits Zig well: put constraints where types and the compiler can understand them, rather than scattering them in comments and runtime checks.
OpenAPI Documentation is a Highlight
I think the thing in wing-app most worth mentioning separately is its built-in OpenAPI generation capability.
According to the documentation, as long as you switch feature routes from wing.Router to openapi.Router and add a documentation parameter when registering routes, you can generate OpenAPI 3.1 documentation.
It provides:
GET /openapi.json: generated OpenAPI documentGET /docs: interactive API documentation page rendered by Scalarzig build openapi: outputs the spec directly to stdout without starting the server or connecting to the database
More importantly, the schema comes from handler signatures.
That means:
wing.Path(...)becomes path parameterswing.Query(...)becomes query parameterswing.Json(T)input becomes request bodywing.Json(T)return becomes 200 responsewing.Created(T)return becomes 201 responseAuth,OptionalAuth,Require(...)will be reflected in security
This is very practical.
The root cause of API documentation rot in many projects is that code and documentation are two separate truths. wing-app's approach is to treat the handler signature as the single source of truth, deriving documentation from the code.
Of course, it currently has some limitations, such as error responses not yet fully entering the documentation, role requirements not automatically written into the spec, and the Scalar page depending on a CDN. But as a Zig Web skeleton, this level of completion is already noteworthy.
Middleware and Error Handling Are Also Restrained
app.zig defines the application's request pipeline:
pub const App = wing.App(AppState, .{
request_scope,
wing.middleware.logger,
wing.middleware.recoverWith(errorStatus),
wing.middleware.route_match,
});
Error-to-HTTP-status-code mapping is also centralized here, for example:
error.InvalidName-> 400error.InvalidCredentials-> 400error.UsernameTaken-> 409- Other errors follow wing's default mapping
This centralized handling is easier to maintain than manually writing status codes in every controller.
Additionally, the README mentions that middleware order is checked at comptime, for example, certain middleware must follow route match. For Zig, this design of "fail at compile time rather than deferring to runtime" is very natural.
Who Is It Suitable For?
I think wing-app is well-suited for three types of people:
First, those who are paying attention to the Zig backend ecosystem.
Through it, you can see how modules like routing, state, database, authentication, and documentation can be combined when writing Web services in Zig.
Second, those who want to write a real Zig Web project.
Compared to building an HTTP server from scratch, this project already provides a fairly complete engineering skeleton. You can first write business logic according to its layering method, then adjust to a feature-first modular structure based on project scale.
Third, framework authors or infrastructure enthusiasts.
wing-app doesn't just showcase business code; it also demonstrates how a Zig Web framework should handle real engineering requirements: typed extractors, typed responses, comptime route assembly, OpenAPI derivation, state projection, auth extractors, etc.
Also Note: It's Still in an Early Stage
Recommendation aside, the boundaries must also be made clear.
wing-app currently depends on local sibling directories for wing and mantle:
.dependencies = .{
.wing = .{ .path = "../wing" },
.mantle = .{ .path = "../mantle" },
}
This indicates it's still in a pre-release co-development stage. Once wing, talon, mantle release official tags, it will be more suitable as a stable dependency locked via URL and hash.
Additionally, it requires Zig 0.16.0, which itself means you need to keep up with Zig version changes.
So my advice is:
- Learning and research: very suitable
- Writing internal tools or experimental services: can try
- Directly going to serious production: you need to evaluate dependency stability, test coverage, deployment environment, and security boundaries yourself
Why Am I Willing to Recommend It?
Because it showcases a direction I really like: writing Web in Zig isn't about copying frameworks from other languages verbatim, but using Zig's own capabilities to reorganize Web engineering.
It emphasizes:
- Explicit dependencies
- Typed request extraction
- Typed responses
- Compile-time assembly
- Clear layering boundaries
- Unavoidable real-service problems like databases and authentication
- Automatic OpenAPI document generation
These things together make it look like a Web project that "can continue to grow."
If you're interested in Zig backend development, go check out this repository:
https://github.com/dacheng-zig/wing-app
You can start with the README, docs/architecture.md, and docs/openapi-user-guide.md, then read the code along src/routes/routes.zig, src/state.zig, src/user/, src/auth/.
Even if you're not ready to use Zig for production services yet, this project is worth a look. It answers a question well: when Zig enters the Web backend domain, what can an engineered application skeleton look like?
Performance Comparison
.net Minimal API
zig wing-app API
Performance test on my Mac M1: .net can reach 5856, while zig can reach 60K+, a 10X gap, indeed exaggerated.