Francis (Francis v0.2.0)

View Source

Module responsible for starting the Francis server and to wrap the Plug functionality

This module performs multiple tasks:

  • Uses the Application module to start the Francis server
  • Defines the Francis.Router which uses Francis.Plug.Router, :match and :dispatch
  • Defines the macros get, post, put, delete, patch and ws to define routes for each operation
  • Setups Plug.Static with the given options
  • Sets up Plug.Parsers with the default configuration of:
    • plug(Plug.Parsers,
        parsers: [:urlencoded, :multipart, :json],
        json_decoder: Jason
      )
  • Defines a default error handler that returns a 500 status code and a generic error message. You can override this by passing the function name on :error_handler option to the use Francis macro which will override the default error handler.

You can also set the following options:

  • :bandit_opts - Options to be passed to Bandit
  • :static - Configure Plug.Static to serve static files
  • :parser - Overrides the default configuration for Plug.Parsers
  • :error_handler - Defines a custom error handler for the server
  • :log_level - Sets the log level for Plug.Logger (default is :info)

Summary

Functions

Defines a DELETE route

Defines a GET route

Retrieves the configuration for a given key, checking both the macro options and the application environment.

Defines a PATCH route

Defines a POST route

Defines a PUT route

Defines an action for umatched routes and returns 404

Defines a WebSocket route with a unified event handler.

Functions

delete(path, handler)

(macro)
@spec delete(String.t(), (Plug.Conn.t() -> binary() | map() | Plug.Conn.t())) ::
  Macro.t()

Defines a DELETE route

Examples

defmodule Example.Router do
  use Francis

  delete "/hello", fn conn ->
    "Hello World!"
  end
end

get(path, handler)

(macro)
@spec get(String.t(), (Plug.Conn.t() -> binary() | map() | Plug.Conn.t())) ::
  Macro.t()

Defines a GET route

Examples

defmodule Example.Router do
  use Francis

  get "/hello", fn conn ->
    "Hello World!"
  end
end

get_configuration(key, opts, default)

@spec get_configuration(atom(), Keyword.t(), any()) :: any()

Retrieves the configuration for a given key, checking both the macro options and the application environment.

patch(path, handler)

(macro)
@spec patch(String.t(), (Plug.Conn.t() -> binary() | map() | Plug.Conn.t())) ::
  Macro.t()

Defines a PATCH route

Examples

defmodule Example.Router do
  use Francis

  patch "/hello", fn conn ->
    "Hello World!"
  end
end

post(path, handler)

(macro)
@spec post(String.t(), (Plug.Conn.t() -> binary() | map() | Plug.Conn.t())) ::
  Macro.t()

Defines a POST route

Examples

defmodule Example.Router do
  use Francis

  post "/hello", fn conn ->
    "Hello World!"
  end
end

put(path, handler)

(macro)
@spec put(String.t(), (Plug.Conn.t() -> binary() | map() | Plug.Conn.t())) ::
  Macro.t()

Defines a PUT route

Examples

defmodule Example.Router do
  use Francis

  put "/hello", fn conn ->
    "Hello World!"
  end
end

unmatched(handler)

(macro)
@spec unmatched((Plug.Conn.t() -> binary() | map() | Plug.Conn.t())) :: Macro.t()

Defines an action for umatched routes and returns 404

ws(path, handler, opts \\ [])

(macro)
@spec ws(
  String.t(),
  (event :: :join | {:close, term()} | {:received, binary()},
   socket :: %{id: binary(), transport: pid(), path: binary(), params: map()} ->
     {:reply, binary() | map() | {atom(), any()}} | :noreply | :ok),
  Keyword.t()
) :: Macro.t()

Defines a WebSocket route with a unified event handler.

The handler function uses pattern matching on events, providing an idiomatic Elixir approach. All events flow through a single function with distinct shapes for easy pattern matching.

Events

The handler receives different event types that can be pattern matched:

  • :join - Sent when a client connects. Return {:reply, message} to send a welcome message.
  • {:close, reason} - Sent when the connection closes. Return :ok or :noreply.
  • {:received, message} - Regular WebSocket text messages from the client.

Messages sent via send(socket.transport, message) are automatically forwarded to the client.

Return Values

  • {:reply, response} - where response can be a binary, a map, or a list (maps/lists will be JSON encoded)
  • :noreply or :ok - to not send a response

Socket State

The socket state map includes:

  • :transport - The transport process that can be used to send messages back to the client using send/2
  • :id - A unique identifier for the WebSocket connection that can be used to track the connection
  • :path - The actual request path of the WebSocket connection (e.g., /chat/general)
  • :params - A map of path parameters extracted from the route (e.g., %{"room" => "general"} for route /:room)

Options

  • :timeout - The timeout for the WebSocket connection in milliseconds (default: 60_000)
  • :heartbeat_interval - The interval in milliseconds between ping frames for heartbeat (default: 30_000). Set to nil to disable heartbeat.

Examples

defmodule Example.Router do
  use Francis

  # Simple echo server
  ws "/echo", fn {:received, message}, socket ->
    {:reply, message}
  end

  # Pattern matching on specific messages
  ws "/ping", fn {:received, "ping"}, socket ->
    {:reply, "pong"}
  end

  # Full lifecycle handling with pattern matching
  ws "/chat/:room", fn
    :join, socket ->
      room = socket.params["room"]
      {:reply, %{type: "welcome", room: room, id: socket.id}}

    {:close, reason}, socket ->
      Logger.info("Client #{socket.id} left: #{inspect(reason)}")
      :ok

    {:received, message}, socket ->
      room = socket.params["room"]
      # Broadcast to self (will be forwarded to client)
      send(socket.transport, "Someone said: " <> message)
      {:reply, "[" <> room <> "] " <> message}
  end

  # JSON responses
  ws "/json", fn {:received, message}, socket ->
    {:reply, %{status: "ok", message: message}}
  end

  # No reply needed
  ws "/fire-and-forget", fn {:received, message}, socket ->
    Logger.info("Received: #{message}")
    :noreply
  end

  # Custom heartbeat interval (ping every 10 seconds)
  ws "/heartbeat", fn {:received, message}, socket ->
    {:reply, message}
  end, heartbeat_interval: 10_000

  # Disable heartbeat
  ws "/no-heartbeat", fn {:received, message}, socket ->
    {:reply, message}
  end, heartbeat_interval: nil
end