This document covers advanced server configuration and extending zigttp with native Zig functions. For handler JavaScript API, CLI options, and virtual modules, see User Guide.
If you embed Server directly in Zig code, these ServerConfig fields tune performance features:
const std = @import("std");
const Server = @import("server.zig").Server;
const ServerConfig = @import("server.zig").ServerConfig;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const config = ServerConfig{
// Pool configuration
.pool_size = 16, // Handler pool size (default: auto)
.pool_wait_timeout_ms = 5, // Max wait for available handler (ms)
.pool_metrics_every = 1000, // Log metrics every N requests (0 = disabled)
// Static file cache configuration
.static_cache_max_bytes = 2 * 1024 * 1024, // Max total cache size (default: 2MB)
.static_cache_max_file_size = 128 * 1024, // Max individual file size (default: 128KB)
// Network configuration
.port = 8080,
.host = "127.0.0.1",
// Logging
.quiet = false, // Disable request logging
// Features
.cors = false, // Enable CORS headers
.static_dir = null, // Static file directory
};
var server = try Server.init(allocator, config);
defer server.deinit();
try server.loadHandler("handler.js");
try server.listen();
}pool_size (default: auto = 2 * cpu_count, min 8) - Number of pre-allocated handler contexts. Memory scales linearly with pool size.
pool_wait_timeout_ms (default: 5) - Maximum time to wait for available handler (milliseconds). Returns 503 if timeout exceeded.
pool_metrics_every (default: 0 = disabled) - Log pool metrics every N requests. Output: Pool metrics: in_use=2/8 exhausted=0 avg_wait_us=3 max_wait_us=20 avg_exec_us=120 max_exec_us=500
static_cache_max_bytes (default: 2MB) - Maximum total cache size. LRU eviction when exceeded.
static_cache_max_file_size (default: 128KB) - Files larger than this are served directly from disk.
Add custom native functions callable from JavaScript by implementing the NativeFn signature:
pub const NativeFn = *const fn(ctx: *Context, this: JSValue, args: []const JSValue) Error!JSValue;const zts = @import("zts");
fn mySquare(ctx: *zts.Context, this: zts.JSValue, args: []const zts.JSValue) !zts.JSValue {
if (args.len < 1) {
return zts.JSValue.fromInt(0);
}
const num = try args[0].toNumber(ctx);
const result = num * num;
return zts.JSValue.fromFloat(result);
}
pub fn registerCustomFunctions(ctx: *zts.Context) !void {
const fn_value = try ctx.createNativeFunction("square", mySquare);
try ctx.setGlobal("square", fn_value);
}Usage in JavaScript:
function handler(request) {
const x = square(5); // Returns 25
return Response.json({ result: x });
}const num: f64 = try value.toNumber(ctx);
const int: i32 = try value.toInt32(ctx);
const str: []const u8 = try value.toString(ctx);
const bool_val: bool = try value.toBoolean(ctx);
const obj: *zts.Object = try value.toObject(ctx);const num = zts.JSValue.fromInt(42);
const float = zts.JSValue.fromFloat(3.14);
const str = try zts.JSValue.fromString(ctx, "hello");
const true_val = zts.JSValue.fromBool(true);
const null_val = zts.JSValue.null();
const undef_val = zts.JSValue.undefined();
const obj = try ctx.createObject();
try obj.setProperty(ctx, "key", zts.JSValue.fromInt(123));fn mayFailFunction(ctx: *zts.Context, this: zts.JSValue, args: []const zts.JSValue) !zts.JSValue {
if (args.len < 1) {
return ctx.throwError("Missing required argument");
}
const value = try args[0].toNumber(ctx);
if (value < 0) {
return ctx.throwError("Value must be non-negative");
}
return zts.JSValue.fromFloat(@sqrt(value));
}- Always use errdefer: Clean up resources on error paths
- Validate arguments: Check argument count and types before conversion
- Use appropriate allocators: Request-scoped allocations should use the context's arena allocator (no manual free needed)
- Handle null and undefined: Check
isNullOrUndefined()before conversion
Compile handlers at build time for fastest cold starts:
zig build -Doptimize=ReleaseFast -Dhandler=examples/handler.tsThis embeds bytecode directly in the binary, eliminating all runtime parsing.