Francis (Francis v0.3.2)
View SourceModule 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, ws and sse 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_handleroption to theuse Francismacro 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 a Server-Sent Events (SSE) route with a unified event handler.
Defines a catch-all action for unmatched routes (returns 404).
Defines a WebSocket route with a unified event handler.
Functions
@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
@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
Retrieves the configuration for a given key, checking both the macro options and the application environment.
@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
@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
@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
@spec sse( String.t(), (event :: :join | {:close, term()} | {:received, term()}, socket :: %{id: binary(), transport: pid(), path: binary(), params: map()} -> {:reply, binary() | map() | list()} | :noreply | :ok), Keyword.t() ) :: Macro.t()
Defines a Server-Sent Events (SSE) route with a unified event handler.
The handler function uses pattern matching on events, providing a consistent
API with the WebSocket macro. SSE connections are unidirectional (server-to-client),
so the handler receives messages via send(socket.transport, message) from other
processes and forwards them to the client as SSE events.
Events
The handler receives different event types that can be pattern matched:
:join- Sent when a client connects. Return{:reply, message}to send an initial event.{:close, reason}- Sent when the connection closes. Return:okor:noreply.{:received, message}- Messages sent tosocket.transportfrom other processes.
Return Values
{:reply, response}- whereresponsecan be:- a binary – sent as
data: <string>\n\n - a map or list – JSON-encoded as
data: <json>\n\n - a map with
:event,:data, and optionally:id/:retrykeys – sent with the corresponding SSE fields
- a binary – sent as
:noreplyor:ok- to not send an event
Socket State
The socket state map includes:
:transport- The transport process PID. Usesend(socket.transport, msg)to push events.:id- A unique identifier for the SSE connection.:path- The actual request path (e.g.,/events/news).:params- A map of path parameters extracted from the route.
Options
:keepalive_interval- Interval in ms between keepalive comments (default: 15_000). Set tonilto disable keepalive.
Examples
defmodule Example.Router do
use Francis
# Simple event stream
sse "/events", fn :join, socket ->
{:reply, %{type: "connected", id: socket.id}}
end
# With named events and full lifecycle
sse "/feed/:topic", fn
:join, socket ->
topic = socket.params["topic"]
{:reply, %{event: "welcome", data: %{topic: topic}}}
{:close, _reason}, _socket ->
:ok
{:received, message}, _socket ->
{:reply, message}
end
# Disable keepalive
sse "/raw", fn {:received, msg}, _socket ->
{:reply, msg}
end, keepalive_interval: nil
end
@spec unmatched((Plug.Conn.t() -> binary() | map() | Plug.Conn.t())) :: Macro.t()
Defines a catch-all action for unmatched routes (returns 404).
@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:okor: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}- whereresponsecan be a binary, a map, or a list (maps/lists will be JSON encoded):noreplyor: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 usingsend/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 tonilto disable heartbeat.:max_frame_size- The maximum allowed size in bytes for incoming WebSocket frames (default: 65_536). Protects against memory exhaustion from oversized messages.
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