HTTP Server
Chuks includes a built-in HTTP server with Express/Fiber-style routing, middleware support, and per-request context integration. Every incoming request runs inside its own isolated Task with automatic cancellation on client disconnect.
Quick Start
Section titled “Quick Start”import { createServer, Request, Response } from "std/http"
const app = createServer()
app.get("/", function(req: Request, res: Response) { res.send("Hello, World!")})
app.listen(3000)$ chuks run server.chuks[2026-01-15 14:30:00] INFO: Chuks HTTP server listening on http://localhost:3000Imports
Section titled “Imports”The std/http module exports the server factory, type definitions, and the HTTP client:
import { createServer, Request, Response } from "std/http"| Export | Kind | Description |
|---|---|---|
createServer | function | Create an HTTP server instance |
Request | dataType | Incoming request object (server-side) |
Response | dataType | Outgoing response object (server-side) |
http | HTTP | HTTP client instance for outbound calls |
HTTP | class | HTTP client class |
HttpResponse | dataType | Response from HTTP client calls |
Type Definitions
Section titled “Type Definitions”dataType Request { method: string // HTTP method (GET, POST, etc.) path: string // Request path (e.g. "/users/123") body: string // Raw request body params: map[string]string // Route parameters (e.g. { id: "123" }) query: map[string]string // Query string parameters headers: map[string]string // Request headers (keys lowercased) ip: string // Client IP address originalUrl: string // Full URL path + query string protocol: string // "http" or "https" hostname: string // Hostname from Host header (port stripped) secure: bool // true when protocol is "https" userAgent: string // User-Agent header value contentType: string // Content-Type header value referer: string // Referer header value cookies: map[string]string // All cookies as key-value pairs formData: map[string]string // Parsed form fields from multipart requests files: []UploadedFile // Uploaded files from multipart/form-data}
dataType UploadedFile { fieldName: string // Form field name (e.g. "avatar") fileName: string // Original filename (e.g. "photo.jpg") contentType: string // MIME type (e.g. "image/jpeg") size: int // File size in bytes data: string // Raw file content extension: string // File extension including dot (e.g. ".jpg")}
dataType Response { statusCode: int // HTTP status code (default 200) body: string // Response body}Route handlers use these types. Request provides read access to the incoming data — including metadata like the client IP, protocol, cookies, uploaded files, and form data; Response provides chainable methods to build the HTTP response.
Creating a Server
Section titled “Creating a Server”import { createServer, Request, Response } from "std/http"
const app = createServer()You can optionally pass a configuration map:
const app = createServer({ maxRequestBodySize: 10 * 1024 * 1024, // 10MB concurrency: 50000, logRequests: true // enabled by default})| Config Key | Type | Default | Description |
|---|---|---|---|
maxRequestBodySize | int | 4MB | Max request body in bytes |
logRequests | bool | true | Enable per-request logging |
logType | string | "terminal" | Log destination: "terminal", "file", or "link" |
filePath | string | "" | Path to log file (when logType is "file") |
fileLink | string | "" | URL endpoint to POST logs (when logType is "link") |
createServer() returns an HttpServer object. All route and middleware methods are chainable:
createServer() .use(logger) .get("/", handler) .listen(3000)Routing
Section titled “Routing”Route Methods
Section titled “Route Methods”| Method | Description |
|---|---|
app.get(path, handler) | Handle GET requests |
app.post(path, handler) | Handle POST requests |
app.put(path, handler) | Handle PUT requests |
app.delete(path, handler) | Handle DELETE requests |
app.patch(path, handler) | Handle PATCH requests |
app.all(path, handler) | Handle any HTTP method |
Route Parameters
Section titled “Route Parameters”Use :param syntax to capture dynamic path segments:
app.get("/users/:id", function(req: Request, res: Response) { const userId = req.params["id"] res.json('{"userId": "' + userId + '"}')})
app.get("/posts/:postId/comments/:commentId", function(req: Request, res: Response) { const post = req.params["postId"] const comment = req.params["commentId"] res.send("Post " + post + ", Comment " + comment)})Route Matching
Section titled “Route Matching”Routes are matched in registration order. The first matching route wins. Static segments must match exactly; :param segments match any non-empty value.
// GET /users/123 → matches, params = { id: "123" }// GET /users → no match (different segment count)// POST /users/123 → no match (wrong method, use .all for any method)Request Object
Section titled “Request Object”Route handlers and middlewares receive a Request object as their first argument.
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
req.method | string | HTTP method (GET, POST, etc.) |
req.path | string | Request path (e.g. “/users/123”) |
req.body | string | Raw request body |
req.params | map[string]string | Route parameters (e.g. { id: "123" }) |
req.query | map[string]string | Query string parameters |
req.headers | map | Request headers |
Methods
Section titled “Methods”| Method | Return | Description |
|---|---|---|
req.header(name) | string | Get a specific header value |
Example
Section titled “Example”app.post("/api/data", function(req: Request, res: Response) { const contentType = req.header("Content-Type") const body = req.body const search = req.query["q"]
res.json('{"received": true}')})Response Object
Section titled “Response Object”The second argument is a Response object used to build the HTTP response. All methods are chainable.
Methods
Section titled “Methods”| Method | Return | Description |
|---|---|---|
res.status(code) | Response | Set the HTTP status code |
res.header(key, value) | Response | Set a response header |
res.json(body) | Response | Send JSON response (sets Content-Type) |
res.send(body) | Response | Send plain text response |
app.get("/api/user", function(req: Request, res: Response) { res.status(200) .header("X-Request-Id", "abc123") .json('{"name": "Alice"}')})Status Codes
Section titled “Status Codes”res.status(201).json('{"created": true}') // Createdres.status(404).send("Not Found") // Not Foundres.status(500).json('{"error": "internal"}') // Server ErrorMiddleware
Section titled “Middleware”Middlewares are functions that run before the route handler. They can inspect/modify the request and response, or short-circuit the chain by not calling next().
Registering Middleware
Section titled “Registering Middleware”app.use(function(req: Request, res: Response, next: any) { println("Request: " + req.method + " " + req.path) next()})Execution Order
Section titled “Execution Order”Middlewares execute in registration order, followed by the matched route handler:
Request → middlewareA → next() → middlewareB → next() → route handler → ResponseThe next() Function
Section titled “The next() Function”Each middleware receives next as its third argument:
- Call
next()— continue to the next middleware or route handler - Don’t call
next()— short-circuit the chain (e.g., auth rejection)
// Auth middleware — short-circuits if no tokenapp.use(function(req: Request, res: Response, next: any) { const token = req.header("Authorization") if (token == "") { res.status(401).json('{"error": "unauthorized"}') return } next()})Common Middleware Patterns
Section titled “Common Middleware Patterns”Logging
Section titled “Logging”The HTTP server includes built-in request logging that is enabled by default. Every request is automatically logged with the method, path, status code, and duration:
[2026-01-15 14:30:00] INFO: GET /users → 200 (0.45ms)[2026-01-15 14:30:01] INFO: POST /users → 201 (1.23ms)[2026-01-15 14:30:02] INFO: GET /unknown → 404 (12µs)For application-level logging, use the std/log module in your handlers:
import { log } from "std/log"
app.get("/users/:id", function(req: Request, res: Response): Response { log.debug("Fetching user", { "id": req.params.id }) return res.json('{"id": "' + req.params.id + '"}')})To disable built-in request logging, pass logRequests: false in the server configuration:
const app = createServer({ logRequests: false })Log Destinations
Section titled “Log Destinations”By default, logs go to the terminal. You can also write logs to a file or send them to a URL endpoint.
Option 1: Configure via .env (zero-config)
When you run chuks new, a .env file is scaffolded with log configuration:
LOG_TYPE=terminal # "terminal", "file", or "link"LOG_FILE_PATH= # Path to log file (when LOG_TYPE=file)LOG_FILE_LINK= # URL to POST logs (when LOG_TYPE=link)The HTTP server automatically reads these environment variables — no code changes needed. Load the .env file with the dotenv module:
import { dotenv } from "std/dotenv"import { createServer } from "std/http"
dotenv.load()const app = createServer()app.listen(3000)Option 2: Configure via createServer()
You can also pass log configuration directly, which overrides environment variables:
// Log to a fileconst app = createServer({ logType: "file", filePath: "./logs/server.log"})
// Send logs to a remote endpoint (async POST with JSON payload)const app = createServer({ logType: "link", fileLink: "https://logs.example.com/ingest"})When logType is "link", each request log is POSTed as JSON:
{ "timestamp": "2026-01-15 14:30:00", "method": "GET", "path": "/users", "status": 200, "duration": "0.45ms"}CORS Headers
Section titled “CORS Headers”app.use(function(req: Request, res: Response, next: any) { res.header("Access-Control-Allow-Origin", "*") res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") res.header("Access-Control-Allow-Headers", "Content-Type, Authorization") next()})Error Handling
Section titled “Error Handling”app.use(function(req: Request, res: Response, next: any) { try { next() } catch (e) { println("Error: " + e) res.status(500).json('{"error": "internal server error"}') }})Server Lifecycle
Section titled “Server Lifecycle”listen(port)
Section titled “listen(port)”Starts the server on the given port. The port argument is required.
app.listen(3000)Graceful Shutdown (Ctrl+C)
Section titled “Graceful Shutdown (Ctrl+C)”When you press Ctrl+C (SIGINT) or send SIGTERM, the server shuts down gracefully:
- The listening socket is closed and the port is released immediately
- The worker pool is drained
- The process exits cleanly
$ chuks run server.chuks[2026-03-15 14:30:00] INFO: Chuks HTTP server listening on http://localhost:3000^C[2026-03-15 14:30:05] INFO: Shutting down server...This means you can immediately restart the server without “address already in use” errors.
Troubleshooting: Address Already in Use
Section titled “Troubleshooting: Address Already in Use”If you see this error, a previous server process is still holding the port:
[ERROR] Server stopped: bind: address already in use
Fix: kill the process holding the port and retry: lsof -ti :3000 | xargs kill -9This can happen if a process was killed ungracefully (e.g., kill -9 or a crash). Run the suggested command to free the port, then restart.
close()
Section titled “close()”Gracefully shuts down the server programmatically. No new connections are accepted, in-flight requests complete, and all request contexts are cancelled.
app.close()Context Integration
Section titled “Context Integration”Every incoming request gets its own Context, forming a hierarchy:
Server Root Context └── Request Context (per request) └── Child Tasks (spawned by handler)Automatic Client Disconnect Cancellation
Section titled “Automatic Client Disconnect Cancellation”When a client disconnects, the request’s context is automatically cancelled. This cascades to any child tasks spawned by the handler and any in-flight HTTP client requests made from within the handler:
import { createServer, Request, Response, http } from "std/http"
const app = createServer()
app.get("/proxy", async function(req: Request, res: Response) { // If the client disconnects, this HTTP call is automatically cancelled const resp = await http.get("https://slow-api.example.com/data") res.json(resp.body)})
app.listen(3000)Context Values
Section titled “Context Values”Each request context is pre-populated with:
| Key | Value |
|---|---|
"method" | HTTP method (GET, POST, etc.) |
"path" | Request path |
Performance
Section titled “Performance”The Chuks HTTP server is powered by gnet (event-loop I/O via epoll/kqueue) and ants (goroutine pool for handler execution). Combined with pooled VM execution, lazy request initialization, and zero-allocation response paths, it achieves Go-beating throughput.
Benchmark Results
Section titled “Benchmark Results”Benchmarked with wrk -t4 -c100 -d10s on Apple M4 Max, macOS, March 2026. GET / → "Hello, World!".
Cross-Language Comparison
Section titled “Cross-Language Comparison”| Runtime | Requests/sec |
|---|---|
| Chuks AOT | 175,742 |
| Bun | 172,190 |
| Node.js | 107,525 |
| Java | 100,367 |
| Go (net/http) | 82,921 |
| Python | 20,001 |
Chuks achieves 2.1× the throughput of Go’s net/http and outperforms every other runtime tested.
Key Takeaways
Section titled “Key Takeaways”- 175,000+ requests/sec sustained on a single machine
- Sub-millisecond latency at low concurrency
- Middleware overhead: ~1-2% — within measurement noise
- Zero errors across all concurrency levels and millions of requests
- 2.1× faster than Go’s net/http by using gnet as the transport layer
- 0% VM overhead compared to a raw gnet Go server
VM Spawn Overhead
Section titled “VM Spawn Overhead”Each request spawns a VM from a sync.Pool. The pool pre-allocates VMs and recycles them:
- Pool acquisition: ~microseconds (atomic CAS, no mutex in fast path)
- VM initialization: Stack pointer reset, frame setup — no allocation
- Pool return:
defer stepVM.Free()returns the VM on completion - Lazy request initialization: Headers, params, query, and body are only built when accessed
At 175K req/s with middleware, each request involves 2 VM spawns (1 middleware + 1 handler) — meaning the runtime sustains 350K+ VM spawn/release cycles per second transparently.
Architecture
Section titled “Architecture”Request Lifecycle
Section titled “Request Lifecycle”1. HTTP request arrives (gnet event loop — epoll/kqueue)2. Route matching on the event loop (fast byte comparison)3. Dispatch to ants goroutine pool4. Acquire Request/Response objects from sync.Pool5. Execute middleware chain: a. Spawn VM from pool for middleware[0] b. Run middleware → calls next() c. Spawn VM from pool for middleware[1] d. ... repeat until route handler e. Spawn VM from pool for route handler6. Write HTTP response via AsyncWrite7. Release Request/Response objects back to poolPer-Step VM Isolation
Section titled “Per-Step VM Isolation”The middleware chain uses per-step VM isolation to prevent stack corruption. When a middleware calls next(), the current VM’s execution is paused while a new VM is spawned for the next step:
Middleware A (VM₁) → next() → Middleware B (VM₂) → next() → Handler (VM₃)New in 0.0.3: Fiber-Style API Additions
Section titled “New in 0.0.3: Fiber-Style API Additions”These features bring Chuks’s HTTP API closer to frameworks like Express and Fiber.
Typed Body Parsing
Section titled “Typed Body Parsing”Parse JSON request bodies into typed dataTypes:
dataType CreateUserRequest { name: string email: string}
app.post("/api/users", function(req: Request, res: Response) { const user = req.parseBody(CreateUserRequest) println(user.name) // Type-safe property access println(user.email) res.status(201).json('{"created": true}')})Cookies
Section titled “Cookies”Set, read, and clear cookies with a clean API:
app.post("/login", function(req: Request, res: Response) { // Set a cookie with options res.cookie("session_id", "abc123", { "maxAge": 86400, "httpOnly": true, "secure": true, "sameSite": "Strict", "path": "/" }).json('{"loggedIn": true}')})
app.get("/dashboard", function(req: Request, res: Response) { // Read a cookie const sessionId = req.cookie("session_id") if (sessionId == "") { res.redirect("/login") return } res.send("Welcome back!")})
app.post("/logout", function(req: Request, res: Response) { // Clear a cookie res.clearCookie("session_id").json('{"loggedOut": true}')})Redirects
Section titled “Redirects”app.get("/old-page", function(req: Request, res: Response) { res.redirect("/new-page") // 302 Found (default)})
app.get("/moved", function(req: Request, res: Response) { res.redirect("/permanent-new", 301) // 301 Moved Permanently})Content-Type Shorthand
Section titled “Content-Type Shorthand”res.type() accepts convenient aliases:
| Shorthand | Expands to |
|---|---|
json | application/json |
html | text/html |
text | text/plain |
xml | application/xml |
form | application/x-www-form-urlencoded |
app.get("/page", function(req: Request, res: Response) { res.type("html").send("<h1>Hello</h1>")})Request Metadata
Section titled “Request Metadata”Access client connection details via new Request properties:
app.get("/info", function(req: Request, res: Response) { const clientIp = req.ip // "127.0.0.1" const fullUrl = req.originalUrl // "/info?debug=true" const proto = req.protocol // "http" or "https" const host = req.hostname // "example.com" const isSecure = req.secure // true/false
res.json('{"ip": "' + clientIp + '", "url": "' + fullUrl + '"}')})Complete Example
Section titled “Complete Example”import { createServer, Request, Response } from "std/http"import { log } from "std/log"
const app = createServer()
// CORS middlewareapp.use(function(req: Request, res: Response, next: any) { res.header("Access-Control-Allow-Origin", "*") res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") next()})
// Built-in request logging is enabled by default — no middleware needed!// Output: [2026-01-15 14:30:00] INFO: GET /api/users/42 → 200 (0.45ms)
// Routesapp.get("/", function(req: Request, res: Response) { res.send("Welcome to the Chuks HTTP Server!")})
app.get("/api/users/:id", function(req: Request, res: Response) { const id = req.params["id"] log.debug("Fetching user", { "id": id }) res.json('{"id": "' + id + '", "name": "Alice"}')})
app.post("/api/users", function(req: Request, res: Response) { const body = req.body res.status(201).json('{"created": true, "body": ' + body + '}')})
app.all("/health", function(req: Request, res: Response) { res.json('{"status": "ok"}')})
app.listen(3000)API Reference
Section titled “API Reference”HttpServer
Section titled “HttpServer”| Method | Signature | Description |
|---|---|---|
use | use(handler) → HttpServer | Register a middleware |
get | get(path, handler) → HttpServer | Register GET route |
post | post(path, handler) → HttpServer | Register POST route |
put | put(path, handler) → HttpServer | Register PUT route |
delete | delete(path, handler) → HttpServer | Register DELETE route |
patch | patch(path, handler) → HttpServer | Register PATCH route |
all | all(path, handler) → HttpServer | Register catch-all route |
listen | listen(port) → void | Start server on given port |
close | close() → void | Graceful shutdown |
Request
Section titled “Request”dataType Request { method: string path: string body: string params: map[string]string query: map[string]string headers: map[string]string ip: string originalUrl: string protocol: string hostname: string secure: bool userAgent: string contentType: string referer: string cookies: map[string]string formData: map[string]string files: []UploadedFile}Properties:
| Property | Type | Description |
|---|---|---|
method | string | HTTP method (GET, POST, etc.) |
path | string | Request path (e.g. “/users/123”) |
body | string | Raw request body |
params | map[string]string | Route parameters (e.g. { id: "123" }) |
query | map[string]string | Query string parameters |
headers | map[string]string | Request headers (keys lowercased) |
ip | string | Client IP address (e.g. “127.0.0.1”) |
originalUrl | string | Full URL path + query string (e.g. “/users?page=1”) |
protocol | string | Protocol string — “http” or “https” |
hostname | string | Hostname from Host header, port stripped |
secure | bool | Shorthand for protocol == "https" |
userAgent | string | Value of the User-Agent header |
contentType | string | Value of the Content-Type header |
referer | string | Value of the Referer header |
cookies | map[string]string | All cookies as key-value pairs |
formData | map[string]string | Parsed form fields from multipart requests |
files | []UploadedFile | Uploaded files from multipart/form-data requests |
Methods:
| Method | Return | Description |
|---|---|---|
header(name) | string | Get a specific header value (case-insensitive) |
cookie(name) | string | Get a cookie value by name |
parseBody(Type?) | any | Parse JSON body, optionally into a typed dataType |
UploadedFile
Section titled “UploadedFile”dataType UploadedFile { fieldName: string fileName: string contentType: string size: int data: string extension: string}| Property | Type | Description |
|---|---|---|
fieldName | string | Form field name (e.g. “avatar”) |
fileName | string | Original filename (e.g. “photo.jpg”) |
contentType | string | MIME type (e.g. “image/jpeg”) |
size | int | File size in bytes |
data | string | Raw file content as a string |
extension | string | File extension including dot (e.g. “.jpg”, “.png”) |
Response
Section titled “Response”dataType Response { statusCode: int body: string}| Method | Signature | Description |
|---|---|---|
status | status(code: int) → Response | Set the HTTP status code (chainable) |
header | header(key, value: string) → Response | Set a response header (chainable) |
json | json(body: any) → Response | Send JSON response with Content-Type header |
send | send(body: string) → Response | Send plain text response |
redirect | redirect(url: string, status?: int) → Response | Redirect to URL (default 302). Sets Location header |
type | type(contentType: string) → Response | Set Content-Type with shorthand (json, html, text, xml, form) |
cookie | cookie(name, value: string, opts?: map) → Response | Set a cookie (options: maxAge, httpOnly, secure, sameSite, path, domain) |
clearCookie | clearCookie(name: string) → Response | Clear a cookie (expires via Max-Age=0) |