smol
Tip: Hover the links for short summaries!
Builder
new, port, random_port, bind, bind_all, after_start
Server Operations
start get_port, get_interface stop
Request Handling
read_bytes_tree, read_bytes, read_string, read_json, read_form, require_method, require_content_type
Response Creation
send_file, send_bytes, send_chunked, send_html, send_json, send_string, send_status, redirect, moved_permanently, server_sent_events, websocket, with_status, with_extra_headers, send, step, close
Advanced
adapt, adapt_node, detect_runtime, status_text
Types
pub type FileError {
IsDir
NoAccess
NoEntry
UnknownFileError
RuntimeNotSupportedFileError
}
Constructors
-
IsDir -
NoAccess -
NoEntry -
UnknownFileError -
RuntimeNotSupportedFileError
smol uses a standard web ReadableStream under the hood on all runtimes,
including Node.js!
pub type ReadableStream
A convenience alias for a HTTP request with a ReadableBody as the body.
pub type Request =
request.Request(ReadableStream)
A convenience alias for a HTTP response with a ReadableBody as the body.
pub type Response =
response.Response(ReadableStream)
pub type SseEvent {
SseEvent(
event: option.Option(String),
data: String,
id: option.Option(String),
retry: option.Option(Int),
)
}
Constructors
-
SseEvent( event: option.Option(String), data: String, id: option.Option(String), retry: option.Option(Int), )
A type used in various functions as a return type from “unfold” or “update”
- style functions, usually used in combination with a
Promise, similar toactor.Nextoryielder.Step.
Represents the result of a stateful iteration that can send messages back to a client.
pub type Step(state, output) {
Close
Step(state: state)
Send(state: state, sending: output)
}
Constructors
-
CloseStop iterating, close the connection. We are done sending.
-
Step(state: state)Loop, without sending anything.
-
Send(state: state, sending: output)Loop, sending
returningto the client.
pub type WebsocketMessage {
Binary(bytes: BitArray)
Text(text: String)
}
Constructors
-
Binary(bytes: BitArray) -
Text(text: String)
Values
pub fn adapt(
handler: fn(request.Request(ReadableStream)) -> promise.Promise(
response.Response(ReadableStream),
),
) -> fn(dynamic.Dynamic) -> promise.Promise(dynamic.Dynamic)
Adapts a smol handler function to work with standard Web Request/Response objects.
This is useful for integrating with other JavaScript frameworks or environments that use the standard Fetch API, like serverless platforms or service workers.
pub fn adapt_node(
handler: fn(request.Request(ReadableStream)) -> promise.Promise(
response.Response(ReadableStream),
),
) -> fn(dynamic.Dynamic, dynamic.Dynamic) -> Nil
Adapts a smol handler function to work with NodeJS.
This can be useful for integrating Gleam and smol into existing Node projects, or for legacy environments where Node is assumed.
pub fn after_start(
builder: Builder,
after_start: fn(Int, String) -> Nil,
) -> Builder
Sets a callback function to be called after the server starts.
The function receives the actual port and interface the server is listening
on. By default, a simple Listening on... message is printed.
Example
fn log_start(port, interface) {
io.println("Server started on " <> interface <> ":" <> int.to_string(port))
}
smol.new(handler)
|> smol.after_start(log_start)
pub fn bind(builder: Builder, interface: String) -> Builder
Sets the network interface to listen on.
By default smol listens on "127.0.0.1" (localhost).
Example
smol.new(handler)
|> smol.bind("192.168.1.100")
pub fn bind_all(builder: Builder) -> Builder
Configures the server to listen on all network interfaces (0.0.0.0).
Example
smol.new(handler)
|> smol.bind_all()
pub fn close() -> promise.Promise(Step(state, output))
pub fn detect_runtime() -> Result(Runtime, Nil)
Detects the current JavaScript runtime environment.
Returns a Result containing the Runtime or an error if the runtime couldn’t be detected.
Example
fn main() {
case smol.detect_runtime() {
Ok(smol.Node) -> io.println("Running on Node.js")
Ok(smol.Deno) -> io.println("Running on Deno")
Ok(smol.Bun) -> io.println("Running on Bun")
Error(_) -> io.println("Unknown runtime")
}
}
pub fn get_interface(server: Server) -> String
Gets the network interface that the server is listening on.
Example
use server <- promise.await(smol.start(builder))
use server <- result.try(server)
let interface = smol.get_interface(server)
io.println("Server listening on interface " <> interface)
pub fn get_port(server: Server) -> Int
Gets the port that the server is listening on.
This is especially useful when using random_port() to determine which
port was assigned.
Example
use server <- promise.await(smol.start(builder))
use server <- result.try(server)
let port = smol.get_port(server)
io.println("Server listening on port " <> int.to_string(port))
pub fn moved_permanently(
to url: String,
) -> promise.Promise(response.Response(ReadableStream))
Respond with a 308 (Moved Permanently) response.
Example
fn handler(request) {
case request.path_segments(request) {
["api", ..rest] -> handle_api(request)
// move all /backend/* requests to /api/*
["backend", ..rest] -> smol.moved_permanently("/api/" <> string.join(rest, "/"))
}
}
pub fn new(
handler: fn(request.Request(ReadableStream)) -> promise.Promise(
response.Response(ReadableStream),
),
) -> Builder
Creates a new server builder with the given request handler.
The server listens on http://localhost:4000 by default. See the examples
pages for example on how to use this function.
pub fn port(builder: Builder, port: Int) -> Builder
Sets the port for the server to listen on.
By default smol tries to listen on port 4000.
Example
smol.new(handler)
|> smol.port(8080)
pub fn random_port(builder: Builder) -> Builder
Configures the server to use a random available port.
Useful for testing to avoid port conflicts.
Example
smol.new(handler)
|> smol.random_port()
pub fn read_bytes(
from request: request.Request(ReadableStream),
up_to max_body_size: Int,
then next: fn(BitArray) -> promise.Promise(
response.Response(ReadableStream),
),
) -> promise.Promise(response.Response(ReadableStream))
Reads the request body as a BitArray and calls the provided callback.
The up_to parameter sets a limit on the size of the body that can be read.
If the body exceeds this limit, a 413 (Payload Too Large) response is sent.
Example
// a simple echo server!
use body <- smol.read_bytes(request, up_to: 1024 * 1024)
smol.send_bytes(body)
pub fn read_bytes_tree(
from request: request.Request(ReadableStream),
up_to max_body_size: Int,
then next: fn(bytes_tree.BytesTree) -> promise.Promise(
response.Response(ReadableStream),
),
) -> promise.Promise(response.Response(ReadableStream))
Reads the request body as a BytesTree and calls the provided callback.
The up_to parameter sets a limit on the size of the body that can be read.
If the body exceeds this limit, a 413 (Payload Too Large) response is sent.
Example
use body <- smol.read_bytes_tree(request, up_to: 1024 * 1024)
// ... do something with the body ...
smol.send_string("Received bytes")
pub fn read_form(
from request: request.Request(ReadableStream),
up_to max_body_size: Int,
then next: fn(List(#(String, String))) -> promise.Promise(
response.Response(ReadableStream),
),
) -> promise.Promise(response.Response(ReadableStream))
Reads and decodes a url-encoded request body, then calls the provided callback.
The up_to parameter sets a limit on the size of the body that can be read.
If the body exceeds this limit, a 413 (Payload Too Large) response is sent.
If the body cannot be decoded as UTF-8, a 400 (Bad Request) response is sent.
If the body cannot be parsed as a query string, a 422 (Unprocessable Entity)
response is sent.
Example
fn handler(request) {
use values <- smol.read_form(request, up_to: 1024 * 1024)
let email = list.key_find(values, "email") |> result.unwrap("")
let password = list.key_find(values, "password") |> result.unwrap("")
// ...
}
pub fn read_json(
from request: request.Request(ReadableStream),
using decoder: decode.Decoder(a),
up_to max_body_size: Int,
then next: fn(a) -> promise.Promise(
response.Response(ReadableStream),
),
) -> promise.Promise(response.Response(ReadableStream))
Reads and decodes a JSON request body, then calls the provided callback.
The decoder parameter is used to decode the JSON into a specific type.
The up_to parameter sets a limit on the size of the body that can be read.
If the body exceeds this limit, a 413 (Payload Too Large) response is sent.
If the body cannot be decoded as UTF-8, a 400 (Bad Request) response is sent.
If the body cannot be parsed as JSON, a 422 (Unprocessable Entity) response is sent.
Example
type User {
User(name: String, age: Int)
}
fn user_decoder() {
use name <- decode.field("name", decode.string)
use age <- decode.field("age", decode.int)
decode.success(User(name:, age:))
}
fn handler(request) {
use user <- smol.read_json(
request,
using: user_decoder(),
up_to: 1024 * 1024,
)
// Process the user
smol.send_string("Hello, " <> user.name)
}
pub fn read_string(
from request: request.Request(ReadableStream),
up_to max_body_size: Int,
then next: fn(String) -> promise.Promise(
response.Response(ReadableStream),
),
) -> promise.Promise(response.Response(ReadableStream))
Reads the request body as a UTF-8 encoded String and calls the provided callback.
The up_to parameter sets a limit on the size of the body that can be read.
If the body exceeds this limit, a 413 (Payload Too Large) response is sent.
If the body cannot be decoded as UTF-8, a 400 (Bad Request) response is sent.
Example
use body <- smol.read_string(request, up_to: 1024 * 1024)
smol.send_string("You sent: " <> body)
pub fn redirect(
to url: String,
) -> promise.Promise(response.Response(ReadableStream))
Respond with a 303 (See Other) response, redirecting the client GET a different url.
Example
fn handler(request) {
use <- smol.require_method(http.Post)
use user <- smol.read_json(request, up_to: 1024 * 1024, using: user_decoder())
use <- promise.await(update_user_in_database(user))
smol.redirect(to: "/users/" <> int.to_string(user.id) <> "/edit")
}
pub fn require_content_type(
from request: request.Request(ReadableStream),
accept content_types: List(String),
then next: fn() -> promise.Promise(
response.Response(ReadableStream),
),
) -> promise.Promise(response.Response(ReadableStream))
A middleware function ensuring that the provided Content-Type header in the
request matches one of the acceptable values.
Returns a 415 (Unsupported Media Type) response if the header does not match.
Example
fn handler(request) {
use <- smol.require_content_type(request, ["application/json"])
use body <- smol.read_json(request, 1024 * 1024, user_decoder())
// ...
}
pub fn require_method(
from request: request.Request(ReadableStream),
require method: http.Method,
then next: fn() -> promise.Promise(
response.Response(ReadableStream),
),
) -> promise.Promise(response.Response(ReadableStream))
A middleware function ensuring that the request has a specific HTTP method.
Returns a 405 (Method Not Allowed) response if the method does not match.
Example
fn handler(request) {
use <- smol.require_method(request, http.Post)
// ...
}
pub fn send(
state state: state,
sending sending: output,
) -> promise.Promise(Step(state, output))
A convenience method useful for async functions.
Equivalent to promise.resolve(Send(state:, returning:))
pub fn send_bytes(
bytes: BitArray,
) -> promise.Promise(response.Response(ReadableStream))
Creates a response with binary data.
The response will have content-type application/octet-stream.
Example
fn handler(request) {
let binary_data = <<1, 2, 3, 4, 5>>
smol.send_bytes(binary_data)
}
pub fn send_chunked(
from state: state,
with fun: fn(state) -> promise.Promise(Step(state, BitArray)),
) -> promise.Promise(response.Response(ReadableStream))
Creates a chunked transfer-encoded HTTP response from a yielder function.
The yielder function produces chunks of data over time, allowing efficient streaming of large responses without loading everything into memory at once.
Note that file responses are automatically streamed.
Example
// Stream a large file in chunks
fn stream_handler(request) {
smol.send_chunked(
from: #(0, file_size),
with: fn(state) {
let #(offset, total) = state
case offset >= total {
// we are done!
True -> smol.close()
False -> {
// Read next chunk (10KB at a time)
let chunk_size = int.min(10240, total - offset)
use chunk <- promise.await(read_chunk(path, offset, chunk_size))
// Return chunk and advance to next offset
smol.send(
state: #(offset + chunk_size, total),
sending: chunk,
)
}
}
}
)
}
pub fn send_file(
path: String,
offset offset: Int,
limit limit: option.Option(Int),
or fallback: fn(FileError) -> promise.Promise(
response.Response(ReadableStream),
),
) -> promise.Promise(response.Response(ReadableStream))
Sends a file as the response.
The path parameter is the file path to send. The path will be joined to
the current working directory. Make sure the path cannot escape the folder
you are trying to serve from!
The offset parameter specifies the starting byte offset (default 0).
The limit parameter specifies the maximum number of bytes to send
(default is the entire file).
Content-Length and Content-Type headers will be set automatically based
on the file name and size.
Example
fn handler(request) {
use _error <- smol.send_file(
"./priv/public/index.html",
offset: 0,
limit: option.None,
)
smol.send_status(404)
}
pub fn send_html(
text: String,
) -> promise.Promise(response.Response(ReadableStream))
Creates a html response with the given string.
The response will have context-type: text/html; charset=utf-8.
Example
fn handler(request) {
smol.send_html("<!doctype html><h1>Hello, World!")
}
pub fn send_json(
json: json.Json,
) -> promise.Promise(response.Response(ReadableStream))
Creates a JSON response with the given JSON data.
The response will have Content-Type: application/json; charset=utf-8.
fn handler(request) {
let user = User(name: "Ada", age: 4)
let data = json.object([
#("name", json.string(user.name)),
#("age", json.int(user.age)),
])
smol.send_json(data)
}
pub fn send_status(
status: Int,
) -> promise.Promise(response.Response(ReadableStream))
A small helper to create a response with the given status code and a standard status message.
Example
fn handler(request) {
smol.send_status(404) // Response with "Not Found" body
}
pub fn send_string(
text: String,
) -> promise.Promise(response.Response(ReadableStream))
Creates a plain text response with the given string.
The response will have context-type: text/plain; charset=utf-8.
Example
fn handler(request) {
smol.send_string("Hello, World!")
}
pub fn server_sent_events(
from state: state,
with fun: fn(state) -> promise.Promise(Step(state, SseEvent)),
) -> promise.Promise(response.Response(ReadableStream))
Creates a Server-Sent Events (SSE) response, producing events using an async yielder function.
The unfold function takes a state and returns either:
Send(new_state, event)to emit an event and continue with new stateStep(new_state)to loop once without emitting an eventCloseto end the stream.
smol also provides the convenience functions send, step, and close which return these values already wrapped in a promise.
Example
fn sse_handler(request) {
// Create a counter that generates events every second
use count <- server_sent_events(0)
use _ <- promise.await(promise.wait(1000))
case count < 10 {
True -> {
let event = SseEvent(
event: Some("update"),
data: "Count is " <> int.to_string(count),
id: None,
retry: None,
)
smol.send(state: count + 1, sending: event)
}
False -> smol.close()
}
}
pub fn start(
builder: Builder,
) -> promise.Promise(Result(Server, Nil))
Starts the server with the configured options.
The returned promise resolves once the server has been started or failed to start.
Example
let handler = fn(request) {
smol.send_string("Hello, Joe!")
}
use server <- promise.await(
smol.new(handler)
|> smol.port(8080)
|> smol.start()
)
case server {
Ok(server) -> {
io.println("Server started!")
}
Error(_) -> {
io.println("Could not start the server")
}
}
pub fn status_text(status: Int) -> String
Returns the status text, given a status code.
If the status code is not known, return the code as a string instead.
Example
smol.status_text(418)
// --> "I'm a teapot"
smol.status_text(911)
// --> "911"
pub fn step(
state state: state,
) -> promise.Promise(Step(state, output))
A convenience method useful for async functions.
Equivalent to promise.resolve(Step(state))
pub fn stop(server: Server) -> promise.Promise(Nil)
Stops the server gracefully.
Returns a Promise that resolves when the server has fully stopped.
Example
use server <- promise.await(smol.start(builder))
case server {
Ok(server) -> {
io.println("Server started, stopping in 10 seconds...")
use _ <- promise.await(promise.wait(10_000))
io.println("Stopping the server...")
use _ <- promise.await(smol.stop(server))
io.println("Server stopped.")
}
Error(_) -> {
io.println("Could not start the server!")
}
}
pub fn websocket(
init init: state,
update update: fn(state, msg) -> promise.Promise(
Step(state, WebsocketMessage),
),
on_open handle_open: fn(fn(msg) -> Nil) -> msg,
on_message handle_message: fn(WebsocketMessage) -> msg,
on_close handle_close: msg,
) -> promise.Promise(response.Response(ReadableStream))
Attempt to open a websocket connection in response to this request.
The client has to have made a valid Upgrade request, otherwise smol will
respond with status 426.
When the upgrade succeeds, an actor-like process is started, sending and receiving messages from the client.
type Msg {
ConnectionOpened(dispatch: fn(Msg) -> Nil)
ReceivedMessage(smol.WebsocketMessage)
ConnectionClosed
}
fn websockets_handler(request) {
use count, msg <- smol.websocket(
init: 1,
on_open: ConnectionOpened,
on_message: ReceivedMessage,
on_close: ConnectionClosed
)
case msg {
ConnectionOpened(_) -> smol.step(state)
ConnectionClosed -> smol.close()
ReceivedMessage(smol.Text(text:)) -> {
let response = "Message No. " <> int.to_string(count) <> ": " <> text
smol.send(state: count + 1, sending: smol.Text(response))
}
ReceivedMessage(smol.Binary(_)) -> smol.step(state: count + 1)
}
}
pub fn with_extra_headers(
response: promise.Promise(response.Response(ReadableStream)),
headers: List(#(String, String)),
) -> promise.Promise(response.Response(ReadableStream))
Convenience function to add headers to a response.
Example
smol.send_send_status(201)
|> smol.with_extra_headers([#("Location", "/user/" <> user_id)])
pub fn with_status(
response: promise.Promise(response.Response(ReadableStream)),
status: Int,
) -> promise.Promise(response.Response(ReadableStream))
Convenience function to change the status code of a response.
Example
smol.send_string("I could not find this :(")
|> smol.with_status(404)