Skip to content

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.

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:3000

The std/http module exports the server factory, type definitions, and the HTTP client:

import { createServer, Request, Response } from "std/http"
ExportKindDescription
createServerfunctionCreate an HTTP server instance
RequestdataTypeIncoming request object (server-side)
ResponsedataTypeOutgoing response object (server-side)
httpHTTPHTTP client instance for outbound calls
HTTPclassHTTP client class
HttpResponsedataTypeResponse from HTTP client calls
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.

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 KeyTypeDefaultDescription
maxRequestBodySizeint4MBMax request body in bytes
logRequestsbooltrueEnable per-request logging
logTypestring"terminal"Log destination: "terminal", "file", or "link"
filePathstring""Path to log file (when logType is "file")
fileLinkstring""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)
MethodDescription
app.get(path, ...middlewares, handler)Handle GET requests
app.post(path, ...middlewares, handler)Handle POST requests
app.put(path, ...middlewares, handler)Handle PUT requests
app.delete(path, ...middlewares, handler)Handle DELETE requests
app.patch(path, ...middlewares, handler)Handle PATCH requests
app.all(path, ...middlewares, handler)Handle any HTTP method

The ...middlewares are optional — you can pass zero or more middleware functions between the path and the handler. See Route-Level Middleware below.

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)
})

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)

Route handlers and middlewares receive a Request object as their first argument.

PropertyTypeDescription
req.methodstringHTTP method (GET, POST, etc.)
req.pathstringRequest path (e.g. “/users/123”)
req.bodystringRaw request body
req.paramsmap[string]stringRoute parameters (e.g. { id: "123" })
req.querymap[string]stringQuery string parameters
req.headersmapRequest headers
MethodReturnDescription
req.header(name)stringGet a specific header value
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}')
})

The second argument is a Response object used to build the HTTP response. All methods are chainable.

MethodReturnDescription
res.status(code)ResponseSet the HTTP status code
res.header(key, value)ResponseSet a response header
res.json(body)ResponseSend JSON response (sets Content-Type)
res.send(body)ResponseSend plain text response
app.get("/api/user", function(req: Request, res: Response) {
res.status(200)
.header("X-Request-Id", "abc123")
.json('{"name": "Alice"}')
})
res.status(201).json('{"created": true}') // Created
res.status(404).send("Not Found") // Not Found
res.status(500).json('{"error": "internal"}') // Server Error

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().

Use app.use() to register middleware that runs on every request:

app.use(function(req: Request, res: Response, next: any) {
println("Request: " + req.method + " " + req.path)
next()
})

Pass one or more middleware functions directly in a route registration, between the path and the handler. These run only for that specific route:

function authMiddleware(req: Request, res: Response, next: any): void {
const token = req.header("Authorization")
if (token == "") {
res.status(401).json('{"error": "unauthorized"}')
return
}
next()
}
function logMiddleware(req: Request, res: Response, next: any): void {
println("[LOG] " + req.method + " " + req.path)
next()
}
// Single route-level middleware
app.get("/protected", authMiddleware, function(req: Request, res: Response) {
res.json('{"secret": "data"}')
})
// Multiple route-level middlewares — executed left to right
app.get("/admin", logMiddleware, authMiddleware, function(req: Request, res: Response) {
res.json('{"admin": true}')
})
// No route-level middleware — only global middleware runs
app.get("/public", function(req: Request, res: Response) {
res.send("Hello!")
})

Middlewares execute in this order: global → group-level → route-level → handler.

Request → global MW → group MW → route MW → route handler → Response

For a route with both global and route-level middleware:

Request → globalMW → next() → authMiddleware → next() → route handler → Response

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 token
app.use(function(req: Request, res: Response, next: any) {
const token = req.header("Authorization")
if (token == "") {
res.status(401).json('{"error": "unauthorized"}')
return
}
next()
})

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 })

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:

.env
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 file
const 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"
}
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()
})
app.use(function(req: Request, res: Response, next: any) {
try {
next()
} catch (e) {
println("Error: " + e)
res.status(500).json('{"error": "internal server error"}')
}
})

Use app.group(prefix, ...middlewares) to create a route group — a set of routes that share a common URL prefix and optional group-level middlewares. This is similar to Gin’s router.Group() or Fiber’s app.Group().

const api = app.group("/api")
api.get("/users", function(req: Request, res: Response) {
// Handles GET /api/users
res.json('[{"name": "Alice"}]')
})
api.post("/users", function(req: Request, res: Response) {
// Handles POST /api/users
res.status(201).json('{"created": true}')
})
api.get("/health", function(req: Request, res: Response) {
// Handles GET /api/health
res.json('{"status": "ok"}')
})

Pass middleware functions after the prefix to apply them to every route in the group:

function adminAuth(req: Request, res: Response, next: any): void {
const token = req.header("Authorization")
if (token == "") {
res.status(403).json('{"error": "forbidden"}')
return
}
next()
}
const admin = app.group("/admin", adminAuth)
admin.get("/dashboard", function(req: Request, res: Response) {
// adminAuth runs before this handler
res.json('{"page": "dashboard"}')
})
admin.get("/settings", function(req: Request, res: Response) {
// adminAuth runs before this handler too
res.json('{"page": "settings"}')
})

Pass multiple middleware functions to apply them all to every route in the group. They execute in the order they are listed:

function logger(req: Request, res: Response, next: any): void {
println("[LOG] " + req.method + " " + req.path)
next()
}
function rateLimiter(req: Request, res: Response, next: any): void {
// rate limiting logic...
next()
}
// All three middlewares run for every route: adminAuth → logger → rateLimiter
const admin = app.group("/admin", adminAuth, logger, rateLimiter)
admin.get("/dashboard", function(req: Request, res: Response) {
res.json('{"page": "dashboard"}')
})

Execution order for every route in this group:

Request → global MW → adminAuth → logger → rateLimiter → handler → Response

Group-level and route-level middlewares compose — group middlewares run first, then route-level middlewares:

function rateLimiter(req: Request, res: Response, next: any): void {
// rate limiting logic...
next()
}
// adminAuth runs first (group-level), then rateLimiter (route-level)
admin.post("/danger", rateLimiter, function(req: Request, res: Response) {
res.json('{"done": true}')
})

Execution order for this route:

Request → global MW → adminAuth (group) → rateLimiter (route) → handler → Response

Groups can be nested. Each child group inherits its parent’s prefix and middlewares:

const api = app.group("/api")
function v2Header(req: Request, res: Response, next: any): void {
res.header("X-API-Version", "2")
next()
}
const v2 = api.group("/v2", v2Header)
v2.get("/users", function(req: Request, res: Response) {
// Handles GET /api/v2/users
// v2Header middleware runs before this handler
res.json('[{"name": "Bob", "version": 2}]')
})

A RouteGroup supports the same route methods as HttpServer, plus use and group for further nesting:

MethodSignatureDescription
getget(path, ...middlewares, handler) → RouteGroupRegister GET route in group
postpost(path, ...middlewares, handler) → RouteGroupRegister POST route in group
putput(path, ...middlewares, handler) → RouteGroupRegister PUT route in group
deletedelete(path, ...mws, handler) → RouteGroupRegister DELETE route in group
patchpatch(path, ...mws, handler) → RouteGroupRegister PATCH route in group
allall(path, ...middlewares, handler) → RouteGroupRegister catch-all route in group
useuse(handler) → RouteGroupAdd middleware to this group
groupgroup(prefix, ...middlewares) → RouteGroupCreate a nested sub-group
import { createServer, Request, Response } from "std/http"
const app = createServer()
// Global middleware
app.use(function(req: Request, res: Response, next: any) {
res.header("X-Powered-By", "Chuks")
next()
})
// Public routes
app.get("/", function(req: Request, res: Response) {
res.send("Welcome!")
})
// API group
const api = app.group("/api")
api.get("/health", function(req: Request, res: Response) {
res.json('{"status": "ok"}')
})
// Admin group with auth middleware
function adminAuth(req: Request, res: Response, next: any): void {
if (req.header("Authorization") == "") {
res.status(403).json('{"error": "forbidden"}')
return
}
next()
}
const admin = app.group("/admin", adminAuth)
admin.get("/dashboard", function(req: Request, res: Response) {
res.json('{"page": "dashboard"}')
})
admin.get("/users", function(req: Request, res: Response) {
res.json('[{"name": "Alice"}]')
})
app.listen(3000)

Starts the server on the given port. The port argument is required.

app.listen(3000)

When you press Ctrl+C (SIGINT) or send SIGTERM, the server shuts down gracefully:

  1. The listening socket is closed and the port is released immediately
  2. The worker pool is drained
  3. 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.

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 -9

This 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.

Gracefully shuts down the server programmatically. No new connections are accepted, in-flight requests complete, and all request contexts are cancelled.

app.close()

Every incoming request gets its own Context, forming a hierarchy:

Server Root Context
└── Request Context (per request)
└── Child Tasks (spawned by handler)

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)

Each request context is pre-populated with:

KeyValue
"method"HTTP method (GET, POST, etc.)
"path"Request path

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.

Benchmarked with wrk -t4 -c100 -d10s on Apple M4 Max, macOS, March 2026. GET /"Hello, World!".

RuntimeRequests/sec
Chuks AOT175,742
Bun172,190
Node.js107,525
Java100,367
Go (net/http)82,921
Python20,001

Chuks achieves 2.1× the throughput of Go’s net/http and outperforms every other runtime tested.

  • 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

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.

1. HTTP request arrives (gnet event loop — epoll/kqueue)
2. Route matching on the event loop (fast byte comparison)
3. Dispatch to ants goroutine pool
4. Acquire Request/Response objects from sync.Pool
5. 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 handler
6. Write HTTP response via AsyncWrite
7. Release Request/Response objects back to pool

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₃)

These features bring Chuks’s HTTP API closer to frameworks like Express and Fiber.

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}')
})

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}')
})
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
})

res.type() accepts convenient aliases:

ShorthandExpands to
jsonapplication/json
htmltext/html
texttext/plain
xmlapplication/xml
formapplication/x-www-form-urlencoded
app.get("/page", function(req: Request, res: Response) {
res.type("html").send("<h1>Hello</h1>")
})

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 + '"}')
})
import { createServer, Request, Response } from "std/http"
import { log } from "std/log"
const app = createServer()
// CORS middleware
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")
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)
// Routes
app.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)
MethodSignatureDescription
useuse(handler) → HttpServerRegister global middleware
getget(path, ...middlewares, handler) → HttpServerRegister GET route
postpost(path, ...middlewares, handler) → HttpServerRegister POST route
putput(path, ...middlewares, handler) → HttpServerRegister PUT route
deletedelete(path, ...middlewares, handler) → HttpServerRegister DELETE route
patchpatch(path, ...middlewares, handler) → HttpServerRegister PATCH route
allall(path, ...middlewares, handler) → HttpServerRegister catch-all route
groupgroup(prefix, ...middlewares) → RouteGroupCreate a route group
listenlisten(port) → voidStart server on given port
closeclose() → voidGraceful shutdown

Returned by app.group() or group.group(). Supports the same route methods as HttpServer.

MethodSignatureDescription
useuse(handler) → RouteGroupAdd middleware to this group
getget(path, ...middlewares, handler) → RouteGroupRegister GET route in group
postpost(path, ...middlewares, handler) → RouteGroupRegister POST route in group
putput(path, ...middlewares, handler) → RouteGroupRegister PUT route in group
deletedelete(path, ...middlewares, handler) → RouteGroupRegister DELETE route in group
patchpatch(path, ...middlewares, handler) → RouteGroupRegister PATCH route in group
allall(path, ...middlewares, handler) → RouteGroupRegister catch-all in group
groupgroup(prefix, ...middlewares) → RouteGroupCreate a nested sub-group
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:

PropertyTypeDescription
methodstringHTTP method (GET, POST, etc.)
pathstringRequest path (e.g. “/users/123”)
bodystringRaw request body
paramsmap[string]stringRoute parameters (e.g. { id: "123" })
querymap[string]stringQuery string parameters
headersmap[string]stringRequest headers (keys lowercased)
ipstringClient IP address (e.g. “127.0.0.1”)
originalUrlstringFull URL path + query string (e.g. “/users?page=1”)
protocolstringProtocol string — “http” or “https”
hostnamestringHostname from Host header, port stripped
secureboolShorthand for protocol == "https"
userAgentstringValue of the User-Agent header
contentTypestringValue of the Content-Type header
refererstringValue of the Referer header
cookiesmap[string]stringAll cookies as key-value pairs
formDatamap[string]stringParsed form fields from multipart requests
files[]UploadedFileUploaded files from multipart/form-data requests

Methods:

MethodReturnDescription
header(name)stringGet a specific header value (case-insensitive)
cookie(name)stringGet a cookie value by name
parseBody(Type?)anyParse JSON body, optionally into a typed dataType
dataType UploadedFile {
fieldName: string
fileName: string
contentType: string
size: int
data: string
extension: string
}
PropertyTypeDescription
fieldNamestringForm field name (e.g. “avatar”)
fileNamestringOriginal filename (e.g. “photo.jpg”)
contentTypestringMIME type (e.g. “image/jpeg”)
sizeintFile size in bytes
datastringRaw file content as a string
extensionstringFile extension including dot (e.g. “.jpg”, “.png”)
dataType Response {
statusCode: int
body: string
}
MethodSignatureDescription
statusstatus(code: int) → ResponseSet the HTTP status code (chainable)
headerheader(key, value: string) → ResponseSet a response header (chainable)
jsonjson(body: any) → ResponseSend JSON response with Content-Type header
sendsend(body: string) → ResponseSend plain text response
redirectredirect(url: string, status?: int) → ResponseRedirect to URL (default 302). Sets Location header
typetype(contentType: string) → ResponseSet Content-Type with shorthand (json, html, text, xml, form)
cookiecookie(name, value: string, opts?: map) → ResponseSet a cookie (options: maxAge, httpOnly, secure, sameSite, path, domain)
clearCookieclearCookie(name: string) → ResponseClear a cookie (expires via Max-Age=0)