Type-safe HTTP server bindings for ReScript using Deno.serve.
rescript-http-server provides idiomatic ReScript bindings to Deno.serve, enabling type-safe HTTP server development without leaving the ReScript ecosystem.
Part of the ReScript Full Stack ecosystem.
-
Type-safe routing - Pattern-based routing with compile-time checks
-
Middleware support - Composable middleware chain (CORS, logging, custom)
-
Request helpers - Type-safe access to method, path, headers, body
-
Response builders - Fluent API for JSON, HTML, redirects, errors
-
Zero runtime overhead - Compiles to clean JavaScript with no wrapper cost
-
Deno-native - Built for Deno’s secure, modern runtime
open HttpServer
let handler = async request => {
switch (method(request), path(request)) {
| (Get, "/") => respond("Hello, World!")
| (Get, "/health") => respondJson(JSON.Encode.object([("status", JSON.Encode.string("ok"))]))
| _ => notFound()
}
}
let server = serve({port: 8000, onListen: info => {
Console.log(`Server running at http://${info["hostname"]}:${Int.toString(info["port"])}`)
}}, handler)open HttpServer
let routes = [
get("/", async _ => respond("Home")),
get("/api/users", async _ => respondJson(usersData)),
post("/api/users", async req => {
let body = await json(req)
// Process body...
respondJson(~status=201, newUser)
}),
get("/api/*", async _ => respondJson(apiIndex)),
]
let server = serve({port: 8000}, router(routes))open HttpServer
let handler = router([
get("/", async _ => respond("Hello")),
get("/data", async _ => respondJson(data)),
])
// Apply middleware chain
let withLogging = withMiddleware(handler, logging())
let withCors = withMiddleware(withLogging, cors(~origin="https://example.com"))
let server = serve({port: 8000}, withCors)| Function | Description | Returns |
|---|---|---|
|
Start HTTP server with options |
|
|
Start server with defaults (port 8000) |
|
|
Gracefully stop server |
|
|
Wait for server to finish |
|
|
Get listening address |
|
| Function | Description | Returns |
|---|---|---|
|
HTTP method as variant |
|
|
URL pathname |
|
|
Query parameters |
|
|
Get header value |
|
|
Read body as text |
|
|
Parse body as JSON |
|
|
Full URL string |
|
|
All headers |
|
| Function | Description |
|---|---|
|
Plain text response |
|
JSON response with correct Content-Type |
|
HTML response |
|
Redirect response (default 302) |
|
404 Not Found |
|
400 Bad Request |
|
401 Unauthorized |
|
403 Forbidden |
|
500 Internal Server Error |
| Function | Description |
|---|---|
|
Create handler from route array |
|
Define GET route |
|
Define POST route |
|
Define PUT route |
|
Define PATCH route |
|
Define DELETE route |
|
Check if path matches pattern |
Pattern matching supports:
-
Exact match:
"/users"matches/users -
Wildcard suffix:
"/api/*"matches/api/anything
type method = Get | Post | Put | Patch | Delete | Head | Options
type handler = request => promise<response>
type middleware = (request, handler) => promise<response>
type route = {
method: method,
pattern: string,
handler: request => promise<response>,
}
type serveOptions = {
port?: int,
hostname?: string,
onListen?: {"port": int, "hostname": string} => unit,
onError?: Error.t => response,
}
type listenInfo = {
port: int,
hostname: string,
}open HttpServer
type user = {id: int, name: string, email: string}
let users = ref([
{id: 1, name: "Alice", email: "alice@example.com"},
{id: 2, name: "Bob", email: "bob@example.com"},
])
let encodeUser = user => JSON.Encode.object([
("id", JSON.Encode.int(user.id)),
("name", JSON.Encode.string(user.name)),
("email", JSON.Encode.string(user.email)),
])
let routes = [
get("/api/users", async _ => {
respondJson(JSON.Encode.array(users.contents, encodeUser))
}),
get("/api/users/*", async req => {
let idStr = path(req)->String.sliceToEnd(~start=12)
switch Int.fromString(idStr) {
| Some(id) =>
switch users.contents->Array.find(u => u.id == id) {
| Some(user) => respondJson(encodeUser(user))
| None => notFound(~body="User not found")
}
| None => badRequest(~body="Invalid user ID")
}
}),
post("/api/users", async req => {
let body = await json(req)
// Validate and create user...
respondJson(~status=201, body)
}),
]
let handler = router(routes)
->withMiddleware(logging())
->withMiddleware(cors())
let _ = serve({port: 8000, onListen: _ => Console.log("API ready")}, handler)open HttpServer
let serveFile = async path => {
try {
let content = await Deno.readTextFile(path)
let contentType = switch {
| path->String.endsWith(".html") => "text/html"
| path->String.endsWith(".css") => "text/css"
| path->String.endsWith(".js") => "application/javascript"
| _ => "text/plain"
}
respondHtml(~headers=Dict.fromArray([("Content-Type", contentType)]), content)
} catch {
| _ => notFound()
}
}
let handler = async req => {
let reqPath = path(req)
let filePath = switch reqPath {
| "/" => "./public/index.html"
| p => "./public" ++ p
}
await serveFile(filePath)
}
let _ = serve({port: 3000}, handler)This package is part of the ReScript Full Stack ecosystem:
| Package | Purpose |
|---|---|
Elm Architecture for frontend |
|
Type-safe PostgreSQL client |
|
WebSocket client/server |
|
Redis client with Streams |
|
Environment variable access |
Following the ReScript Full Stack design philosophy:
-
No global state - Configuration passed explicitly
-
Interface files -
.residefines public API boundary -
Minimal dependencies - Only
@rescript/corerequired -
Configuration over convention - Explicit options, no magic
-
Async-first - All I/O returns promises
SPDX-License-Identifier: AGPL-3.0-or-later
Copyright © 2025 Hyperpolymath
See LICENSE.txt for full terms.
See CONTRIBUTING.adoc for guidelines.