Server API ​
The httpx.zig server module provides a robust HTTP server with middleware support, routing, and proper context handling. The high-level runtime supports HTTP/1.0, HTTP/1.1, HTTP/2, and HTTP/3.
Protocol Support ​
| Protocol | Status | Features |
|---|---|---|
| HTTP/1.0 | ✅ Full | Basic request/response |
| HTTP/1.1 | ✅ Full | Keep-Alive, chunked transfer, pipelining |
| HTTP/2 | ✅ Full | High-level server runtime over TCP plus full framing/HPACK/stream primitives |
| HTTP/3 | ✅ Full | High-level server runtime over UDP plus full HTTP/3/QPACK/QUIC primitives |
Server ​
The Server struct manages the listener, router, and middleware processing.
Initialization ​
const std = @import("std");
const httpx = @import("httpx");
const io = std.Io.Threaded.global_single_threaded.io();
// Initialize with default config
var server = try httpx.Server.init(allocator, io, .{});
defer server.deinit();
// Shorthand: explicit allocator + config
var server = try httpx.Server.init(allocator, io, .{
.port = 3000,
.host = "0.0.0.0",
});
defer server.deinit();
```zig
const std = @import("std");
const httpx = @import("httpx");
const io = std.Io.Threaded.global_single_threaded.io();
// Initialize with default config
var server = try httpx.Server.init(allocator, io, .{});
defer server.deinit();
// Initialize with custom config
var server = try httpx.Server.init(allocator, io, .{
.host = "127.0.0.1",
.port = 3000,
.portStrategy = .incremental,
.maxPortAttempts = 32,
.maxBody = 1024 * 1024, // 1MB
});
defer server.deinit();Configuration (ServerConfig) ​
| Field | Type | Default | Description |
|---|---|---|---|
host | []const u8 | "0.0.0.0" | Interface to bind to. |
port | u16 | 8080 | Port to listen on (0 = ephemeral; read back via localPort()). |
portStrategy | PortStrategy | .incremental | Startup behavior when the port is occupied (.incremental, .strict, .exit). |
maxPortAttempts | u16 | 50 | Port candidates to try when portStrategy = .incremental. |
maxBody | usize | 8MB | Largest accepted request body. |
maxConnections | usize | 0 | Total connections run() serves before returning (0 = unlimited; tests use small values to join deterministically). |
maxRequestsPerConn | usize | 1000 | Max requests served per keep-alive connection. |
keepAlive | bool | false | Honor HTTP/1.1 keep-alive on connections. |
allowLfLineEndings | bool | false | Accept bare LF line endings when parsing requests. |
trustForwardedHeaders | bool | false | Trust X-Forwarded-* headers from reverse proxies. |
trustedProxies | []const []const u8 | &.{} | IPs/CIDRs allowed to supply forwarded headers. |
httpVersion | ?HttpVersion | null | Preferred HTTP version enforcement. |
http10 | bool | true | Enable HTTP/1.0 handling. |
http11 | bool | true | Enable HTTP/1.1 handling. |
http2 | bool | true | HTTP/2 cleartext/TLS server runtime path. |
http3 | bool | false | HTTP/3 server runtime path. |
logging | LoggingOptions | {} | Event callback + level (silent by default). |
enableDocs | bool | true | Mount /openapi.json, /docs, /redoc by default. |
docs | ?docs.Config | null | Overrides for docs routes when enabled. |
docsTitle | []const u8 | "HTTPX API" | Title used by the docs UI. |
watch | bool | false | Watch a directory and broadcast changes. |
watchDir | []const u8 | "." | Directory to watch when watch is enabled. |
liveReload | bool | false | Enable SSE/WebSocket live-reload endpoints + script injection. |
liveReloadPath | []const u8 | "/__httpx_liveReload" | Mount path for the live-reload endpoint. |
templates | ?TemplateConfig | null | Template engine config (auto-discovers templates/ when null). |
tls | ?tls.Server.Config | null | TLS/HTTPS config (certificate + private key). |
All ServerConfig fields are optional customizations. Omitted fields use the built-in defaults; . {} is the safe default.
Server Lifecycle ​
server.run(); // blocking accept loop
const thread = try server.start(); // non-blocking, returns std.Thread
server.pause(); // pause accepting new connections
server.resumeAccepting(); // resume accepting new connections
server.requestShutdown(); // graceful shutdown (drains in-flight requests)
server.stop(); // immediate shutdown
const port = server.localPort(); // actual bound port (useful with .port = 0)HTTP/2 and HTTP/3 Runtime Configuration ​
const io = std.Io.Threaded.global_single_threaded.io();
var server = try httpx.Server.init(allocator, io, .{
.host = "127.0.0.1",
.port = 8080,
.http2 = true,
.http3 = false,
});
defer server.deinit();Port Conflict Handling ​
const io = std.Io.Threaded.global_single_threaded.io();
var server = try httpx.Server.init(allocator, io, .{
.host = "127.0.0.1",
.port = 8080,
.portStrategy = .incremental,
.maxPortAttempts = 32,
});
defer server.deinit();
server.run();.strict: return an error immediately if bind fails..incremental: tryport + 1,port + 2, ... until success or attempts are exhausted..exit: fail if the port is occupied.
Methods ​
run ​
Starts the server. This method blocks.
server.run();start ​
Starts the server in a background thread. Returns a thread handle that can be joined.
const thread = try server.start();
// Server is now running in the background
// ...
server.stop(); // Stop when done
thread.join(); // Wait for the thread to finishlocalPort ​
Returns the effective bound port (useful with .port = 0 or portStrategy = .incremental).
const p = server.localPort();
_ = p;stop ​
Stops the server gracefully.
server.stop();use ​
Adds a middleware to the global stack.
try server.use(httpx.middleware.logging);
try server.use(httpx.middleware.cors);Logging ​
HTTPX never prints on its own. Configure the server event callback to observe requests (see Observability: Logging):
fn onEvent(event: httpx.ServerEvent) void {
if (event.kind == .requestCompleted) {
std.debug.print("{s} {s} {d}\n", .{ event.method, event.path, event.status });
}
}
var server = try httpx.Server.init(allocator, io, .{
.host = "127.0.0.1",
.port = 8080,
.logging = .{ .callback = onEvent },
});Omit .logging.callback (the default) for fully silent operation.
Routing Methods ​
| Method | Description |
|---|---|
get(path, handler) | Register GET route |
post(path, handler) | Register POST route |
put(path, handler) | Register PUT route |
delete(path, handler) | Register DELETE route |
patch(path, handler) | Register PATCH route |
head(path, handler) | Register HEAD route |
options(path, handler) | Register OPTIONS route |
use(mw) | Register global middleware |
static(mount, dir) | Serve a filesystem directory |
spa(mount, dir) | Serve a directory with SPA fallback |
metrics(path) | Mount the Prometheus endpoint |
setStatusHandler(code, handler) | Custom handler per status code |
Quick Example ​
const std = @import("std");
const httpx = @import("httpx");
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const io = std.Io.Threaded.global_single_threaded.io();
var server = try httpx.Server.init(allocator, io, .{});
defer server.deinit();
// Add middleware
try server.use(httpx.middleware.logging);
try server.use(httpx.middleware.cors);
// Register routes
try server.get("/", homePage);
try server.get("/api/users", listUsers);
try server.post("/api/users", createUser);
try server.get("/api/users/{id}", getUser);
try server.put("/api/users/{id}", updateUser);
try server.delete("/api/users/{id}", deleteUser);
std.debug.print("Server listening on http://localhost:8080\n", .{});
server.run();
}
fn homePage(ctx: *httpx.Context) anyerror!httpx.Response {
return ctx.html("<h1>Welcome to httpx.zig!</h1>");
}
fn listUsers(ctx: *httpx.Context) anyerror!httpx.Response {
return ctx.renderJson(.{ .users = &.{} });
}Explicit Server Types ​
SSE Responses ​
Server-Sent Events use httpx.sse.Writer.EventWriter over a normal handler response (see examples/sseServer.zig and SSE). There is no ctx.sse(...) helper; parse inbound streams with httpx.sse.Parser (eventType, data, id, retryMs fields on parsed events).
Route Groups ​
Register versioned route families explicitly (no group helper object):
try server.get("/api/v1/users", listUsers);
try server.post("/api/v1/users", createUser);
try server.get("/api/v1/users/{id}", getUser);Path parameters use {name} segments and are read with ctx.param("id").
Custom 404 Handler ​
const router_mod = httpx.web.router;
var router = router_mod.Router.init(allocator);
defer router.deinit();
router.setNotFoundHandler(notFound);
fn notFound(ctx: *httpx.Context) anyerror!httpx.Response {
return ctx.textStatus(404, "Not Found");
}Context ​
The Context struct is passed to every route handler and middleware.
Fields ​
| Field | Type | Description |
|---|---|---|
allocator | Allocator | Request-scoped allocator |
path | []const u8 | Clean request path |
method | Method | Request method |
headers | []Header | Request headers |
body | []u8 | Request body bytes |
isTls | bool | Whether the connection uses TLS |
Request Accessors ​
| Method | Description |
|---|---|
param(name) | Get URL path parameter ({id}) |
queryParam(name) | Get query string parameter |
header(name) | Get request header value (case-insensitive) |
cookie(name) | Get request cookie value by name |
bearerToken() | Parse Authorization: Bearer <token> |
basicAuth() | Parse Authorization: Basic ... into username/password |
remoteAddress() | Peer address (honors trusted X-Forwarded-*) |
scheme() | "https" for TLS, else "http" |
host() | Request host |
json(T) | Parse request body as typed JSON into T |
Response Helpers ​
| Method | Description |
|---|---|
html(content) / htmlStatus(code, content) | HTML responses |
text(content) / textStatus(code, content) | Plain-text responses |
renderJson(value) / renderJsonStatus(code, value) | JSON responses |
render(name, data) / renderStatus(code, name, data) | Template responses |
xml(content) / rss(content) / atom(content) | Feed responses |
robots(content) / sitemap(content) | Crawler responses |
binary(bytes, contentType) | Binary responses |
custom(code, contentType, content) | Fully custom responses |
redirect(location, code) | Redirect responses |
jsonFmt(fmt, args) | Formatted JSON responses |
Example Context Usage ​
fn getUser(ctx: *httpx.Context) anyerror!httpx.Response {
// Get URL parameter
const id = ctx.param("id") orelse return ctx.textStatus(400, "Missing user ID");
// Get query parameter
const format = ctx.queryParam("format") orelse "json";
_ = format;
// Get request header
const auth = ctx.header("Authorization");
_ = auth;
// Return JSON response
return ctx.renderJson(.{
.id = id,
.name = "John Doe",
.email = "john@example.com",
});
}Handlers ​
Handlers are functions that take a *Context and return a !Response.
const Handler = *const fn (*Context) anyerror!Response;
fn myHandler(ctx: *httpx.Context) anyerror!httpx.Response {
return ctx.renderJson(.{ .message = "Hello World" });
}Handler Patterns ​
// Simple text response
fn hello(ctx: *httpx.Context) anyerror!httpx.Response {
return ctx.text("Hello, World!");
}
// JSON response with status
fn created(ctx: *httpx.Context) anyerror!httpx.Response {
return ctx.renderJsonStatus(201, .{ .id = 1, .created = true });
}
// Redirect
fn redirectHome(ctx: *httpx.Context) anyerror!httpx.Response {
return ctx.redirect("/", 302);
}
// Error handling
fn riskyHandler(ctx: *httpx.Context) anyerror!httpx.Response {
const data = doSomethingRisky() catch |err| {
return ctx.renderJsonStatus(500, .{
.error = "Internal Server Error",
.message = @errorName(err),
});
};
return ctx.renderJson(data);
}Static Files ​
Serve directories with server.static(mount, dir) (filesystem, with embedded-asset fallback) instead of hand-rolled file handlers:
// Serves ./public/* under /static with ETag + conditional GET.
try server.static("/static", "./public");For runnable demos see examples/staticFiles.zig, examples/staticSite.zig, and examples/staticEmbedded.zig (single-file embedded mode).
Error Handling ​
Handle route-level errors in handlers and return explicit status codes as needed:
fn handler(ctx: *httpx.Context) anyerror!httpx.Response {
const result = riskyOperation() catch |err| switch (err) {
error.NotFound => return ctx.renderJsonStatus(404, .{ .error = "Not Found" }),
error.Unauthorized => return ctx.renderJsonStatus(401, .{ .error = "Unauthorized" }),
else => return ctx.renderJsonStatus(500, .{ .error = "Internal Server Error" }),
};
return ctx.renderJson(result);
}See Also ​
- Middleware API - Built-in middleware
- Router API - Advanced routing
- Protocol API - HTTP/2, HTTP/3
- Server Guide - Usage guide
