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 opaque type Builder
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 Runtime {
  Node
  Deno
  Bun
}

Constructors

  • Node
  • Deno
  • Bun
pub opaque type Server
pub type SseEvent {
  SseEvent(
    event: option.Option(String),
    data: String,
    id: option.Option(String),
    retry: option.Option(Int),
  )
}

Constructors

A type used in various functions as a return type from “unfold” or “update”

  • style functions, usually used in combination with a Promise, similar to actor.Next or yielder.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

  • Close

    Stop iterating, close the connection. We are done sending.

  • Step(state: state)

    Loop, without sending anything.

  • Send(state: state, sending: output)

    Loop, sending returning to 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))

A convenience method useful for async functions.

Equivalent to promise.resolve(Close)

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 state
  • Step(new_state) to loop once without emitting an event
  • Close to 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)
Search Document