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

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

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

Middlewares execute in registration order, followed by the matched route handler:

Request → middlewareA → next() → middlewareB → 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"}')
}
})

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 a middleware
getget(path, handler) → HttpServerRegister GET route
postpost(path, handler) → HttpServerRegister POST route
putput(path, handler) → HttpServerRegister PUT route
deletedelete(path, handler) → HttpServerRegister DELETE route
patchpatch(path, handler) → HttpServerRegister PATCH route
allall(path, handler) → HttpServerRegister catch-all route
listenlisten(port) → voidStart server on given port
closeclose() → voidGraceful shutdown
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)