跪拜 Guibai
← Back to the summary

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:

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:

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:

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:

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:

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:

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:

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:

More importantly, the schema comes from handler signatures.

That means:

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:

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:

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:

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

.net.png

zig wing-app API

zig.png

Performance test on my Mac M1: .net can reach 5856, while zig can reach 60K+, a 10X gap, indeed exaggerated.