smol

Tip: Hover the links for short summaries!

Builder

new, port, random_port, bind, bind_all, after_start, on_error

Server Operations

start get_port, get_interface stop

Request Handling

read_bytes_tree, read_bytes, read_string, read_json, read_form, read_multipart, handle_head, cache, cache_with, enable_cross_origin_requests, csrf_known_header_protection, content_security_policy_protection, 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, last_modified, with_extra_headers, as_file_download, send, step, close

Advanced

adapt, adapt_node, detect_runtime, rescue status_text

Types

pub type Builder =
  @internal Builder

Configuration for the enable_cross_origin_requests middleware.

origins must contain trusted origin strings such as "https://app.example.com". Origins are compared literally. This middleware does not expand wildcard or suffix patterns. The string "*" has no special meaning and only matches an origin header with that exact value.

pub type CrossOriginPolicy {
  CrossOriginPolicy(
    origins: List(String),
    methods: List(http.Method),
    headers: List(String),
    expose: List(String),
    max_age: option.Option(Int),
    credentials: Bool,
  )
}

Constructors

  • CrossOriginPolicy(
      origins: List(String),
      methods: List(http.Method),
      headers: List(String),
      expose: List(String),
      max_age: option.Option(Int),
      credentials: Bool,
    )

    Arguments

    origins

    Exact origins that are allowed to read responses.

    methods

    Methods that are allowed for cross-origin requests.

    headers

    Request headers that are allowed in cross-origin preflight requests.

    expose

    Response headers that browsers may expose to JavaScript.

    max_age

    Optional Access-Control-Max-Age for accepted preflight requests.

    credentials

    Whether cookies and other credentials are allowed.

pub type FileError {
  IsDir
  NoAccess
  NoEntry
  UnknownFileError
  RuntimeNotSupportedFileError
}

Constructors

  • IsDir
  • NoAccess
  • NoEntry
  • UnknownFileError
  • RuntimeNotSupportedFileError

A parsed multipart form.

pub type FormData {
  FormData(
    values: List(#(String, String)),
    files: List(#(String, UploadedFile)),
  )
}

Constructors

  • FormData(
      values: List(#(String, String)),
      files: List(#(String, UploadedFile)),
    )

smol uses a standard web ReadableStream under the hood on all runtimes, including Node.js!

pub type ReadableStream =
  @internal 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 UploadedFile {
  UploadedFile(file_name: String, path: String)
}

Constructors

  • UploadedFile(file_name: String, path: String)
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 as_file_download(
  response: promise.Promise(response.Response(ReadableStream)),
  named name: String,
) -> promise.Promise(response.Response(ReadableStream))

Convenience function to send a response as a file download.

Sets the Content-Disposition header to attachment using the given file name.

Example

smol.send_string("{\"ok\":true}")
|> smol.as_file_download(named: "blob.json")
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 cache(
  from request: request.Request(ReadableStream),
  then next: fn() -> promise.Promise(
    response.Response(ReadableStream),
  ),
) -> promise.Promise(response.Response(ReadableStream))

Handles conditional cache requests.

This middleware adds Cache-Control: no-cache when the response does not already have a Cache-Control header, and handles If-None-Match and If-Modified-Since request headers using the response’s ETag and Last-Modified headers.

For expensive responses where you can compute the validators before rendering the body, use cache_with to allow the middleware to return 304 Not Modified before calling the handler.

Example

fn handler(request) {
  use <- smol.cache(request)

  use _error <- smol.send_file(
    "./priv/public/index.html",
    offset: 0,
    limit: option.None,
  )

  smol.send_status(404)
}
pub fn cache_with(
  from request: request.Request(ReadableStream),
  etag etag: option.Option(String),
  last_modified last_modified: option.Option(timestamp.Timestamp),
  then next: fn() -> promise.Promise(
    response.Response(ReadableStream),
  ),
) -> promise.Promise(response.Response(ReadableStream))

Handles conditional cache requests with explicit validators.

This is the same as cache, but it can return 304 Not Modified before calling the handler when the request validators match the explicit etag or last_modified values.

The final response is still checked after the handler runs, using its ETag and Last-Modified headers. If the final response is missing one of the explicit validators, it is added automatically.

Example

fn handler(request) {
  use <- smol.cache_with(
    request,
    etag: option.None,
    last_modified: option.Some(post.updated_at),
  )

  render_post(post)
}
pub fn close() -> promise.Promise(Step(state, output))

A convenience method useful for async functions.

Equivalent to promise.resolve(Close)

pub fn content_security_policy_protection(
  then handle_request: fn(String) -> promise.Promise(
    response.Response(ReadableStream),
  ),
) -> promise.Promise(response.Response(ReadableStream))

Protects against cross-site-scripting (XSS) attacks using a nonce-based content-security-policy (CSP).

This middleware will provide a unique single use random string (a nonce) to the handler, and set this CSP header on the response returned by the handler.

Content-Security-Policy:
  script-src 'nonce-{NONCE}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

This header causes the browser to be restricted in these ways:

  • Any <script> tag without a nonce="..." property set to the nonce for this request will not be executed. Any scripts created by scripts with the correct nonce property will be executed.

  • Any inline JavaScript event handlers on elements will not be evaluated. e.g. <span onclick="doSomething();">Click me</span> will do nothing when clicked.

  • Any <object> or <embed> elements will not be executed.

  • Any use of <base> to change the base for relative URLs will be prevented.

When using this middleware be sure to add the nonce="..." property to all <script> elements.

use csp_nonce <- smol.content_security_policy_protection()
<script type="module" nonce="RENDER_YOUR_CSP_NONCE_HERE">
  console.log("Hello, Joe!")
</script>

It is recommended to add this middleware so that it applies to all routes in your application.

For more information about CSP see these articles:

pub fn csrf_known_header_protection(
  from request: request.Request(ReadableStream),
  then next: fn(request.Request(ReadableStream)) -> promise.Promise(
    response.Response(ReadableStream),
  ),
) -> promise.Promise(response.Response(ReadableStream))

Protects against Cross-Site Request Forgery (CSRF) attacks by checking the host request header against the origin header or referer header.

  • Requests with the Get and Head methods are accepted.
  • Requests with no host header are rejected with status 400: Bad Request.
  • Requests with no origin or referer headers are accepted, but have the cookie header removed to prevent CSRF attacks against cookie based sessions.
  • Requests with origin/referer headers that match their host header are accepted.
  • Requests with headers that don’t match are rejected with status 400: Bad Request.

This middleware implements the OWASP Verifying Origin With Standard Headers CSRF defense-in-depth technique. Do not allow Get or Head requests to trigger side effects if relying only on this function and the SameSite cookies feature for CSRF protection.

This middleware and SameSite cookies typically is sufficient to protect against CSRF attacks, but you may decide to employ token based mitigation for more complete CSRF defence-in-depth.

If you have routes that should be accessible from other origins then you should not use this middleware for those routes.

Example

fn handler(request) {
  use request <- smol.csrf_known_header_protection(request)
  // ...
}
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 enable_cross_origin_requests(
  from request: request.Request(ReadableStream),
  using policy: CrossOriginPolicy,
  then next: fn(request.Request(ReadableStream)) -> promise.Promise(
    response.Response(ReadableStream),
  ),
) -> promise.Promise(response.Response(ReadableStream))

Enables cross-origin requests.

This middleware adds CORS response headers for requests that match the given policy. Requests that do not match the policy are passed through without CORS allow headers, so browsers will not expose their responses to the calling JavaScript. Preflight requests are answered directly with an empty 204 response.

CORS controls whether browsers expose responses to scripts from other origins. It does not replace authentication, authorization, or CSRF protection.

Example

fn handler(request) {
  use request <- smol.enable_cross_origin_requests(
    request,
    using: smol.CrossOriginPolicy(
      origins: ["https://app.example.com"],
      methods: [http.Get, http.Post],
      headers: ["content-type"],
      expose: [],
      max_age: option.Some(600),
      credentials: False,
    ),
  )
  // ...
}
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 handle_head(
  from request: request.Request(ReadableStream),
  then next: fn(request.Request(ReadableStream)) -> promise.Promise(
    response.Response(ReadableStream),
  ),
) -> promise.Promise(response.Response(ReadableStream))

Converts HEAD requests to GET requests, then calls the provided callback.

This allows a handler for a GET route to also answer HEAD requests with the same status and headers. The x-original-method header is set to "HEAD" on the request passed to the callback.

Example

fn handler(request) {
  use request <- smol.handle_head(request)
  // ...
}
pub fn last_modified(
  response: promise.Promise(response.Response(ReadableStream)),
  at timestamp: timestamp.Timestamp,
) -> promise.Promise(response.Response(ReadableStream))

Convenience function to set the Last-Modified header on a response from a Timestamp.

Pairs with the cache middleware, which uses the header to respond with 304 Not Modified for conditional requests.

Example

smol.send_html(render_post(post))
|> smol.last_modified(at: post.updated_at)
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 on_error(
  builder: Builder,
  on_error: fn(String, dynamic.Dynamic) -> Nil,
) -> Builder

Sets a callback function to be called whenever an error happens.

The function receives a string containing some context as well as the raw javascript error value.

By default, errors are logged to standard error.

Example

fn error_handler(message, error) {
  io.println("Errror: " <> message <> ": " <> string.inspect(error))
}

smol.new(handler)
|> smol.error_handler(error_handler)
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_multipart(
  from request: request.Request(ReadableStream),
  up_to max_body_size: Int,
  files_up_to max_file_size: Int,
  then next: fn(FormData) -> promise.Promise(
    response.Response(ReadableStream),
  ),
) -> promise.Promise(response.Response(ReadableStream))

Reads and decodes a multipart form 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. The files_up_to parameter sets a limit on the size of each uploaded file. If any file exceeds this limit, a 413 (Payload Too Large) response is sent. If the multipart form cannot be parsed, a 422 (Unprocessable Entity) response is sent.

Uploaded files are written to temporary files. They are available while the callback is running and are cleaned up after it completes.

Example

fn handler(request) {
  use values <- smol.read_multipart(
    request,
    up_to: 1024 * 1024,
    files_up_to: 512 * 1024,
  )
  // ...
}
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 rescue(
  on_error: fn(dynamic.Dynamic) -> promise.Promise(
    response.Response(ReadableStream),
  ),
  inner: fn() -> promise.Promise(
    response.Response(ReadableStream),
  ),
) -> promise.Promise(response.Response(ReadableStream))

A middleware function that allows you to recover from an exception thrown inside the inner request handler.

Example

fn handler(request) {
  use <- smol.rescue(fn(_err) { smol.send_status(500) })
  // ...
}
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. ETag and Last-Modified headers will also be set automatically based on the file size and modification time.

Use the cache middleware to respond with 304 Not Modified when the request’s validators match the generated headers.

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