filipecabaco
Francis - Sinatra-style HTTP / WebSocket / SSE on Plug + Bandit
Hi everyone,
I’ve been working on Francis, a lightweight framework built on top of Plug and Bandit that tries to get you from idea to a running endpoint with as little ceremony as possible. Think Sinatra for Elixir.
What it is
You use Francis in a module, define routes with get/post/ws/sse, and you have a running server. No routers, no controllers, no context boilerplate, just a function per route.
defmodule MyApp do
use Francis
get "/", fn _ -> %{hello: :world} end
get "/:name", fn %{params: %{"name" => name}} -> "hello #{name}" end
post "/", fn conn -> conn.body_params end
ws "/chat", fn
:join, socket -> {:reply, "welcome"}
{:received, msg}, _ -> {:reply, msg}
end
sse "/events", fn
:join, socket -> {:reply, %{id: socket.id}}
{:received, m}, _ -> {:reply, m}
end
unmatched fn _ -> "not found" end
end
Return a string and it renders as HTML. Return a map and it renders as JSON. Return a Plug.Conn for full control.
Get started
mix archive.install hex francis
mix francis.new my_app
cd my_app
mix francis.server
Or add to an existing project:
{:francis, “~> 0.3”}
If you use Claude Code, install the Francis skill for route patterns, SSE/WS gotchas, and security guidance:
npx skills add francis-build/francis@francis-thinking
Links
- Hex: francis | Hex
- Docs: Francis v0.3.3 — Documentation
- Site: https://francis.build
Curious what the community thinks. It is not trying to replace Phoenix, it is for when you want something closer to raw Plug without writing the same boilerplate every time. Happy to answer questions.
Most Liked
filipecabaco
Ok I did try out and it does work as you said
since it’s just Plug it’s all good
defmodule PhoenixEmbedWeb.Router do
use PhoenixEmbedWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {PhoenixEmbedWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", PhoenixEmbedWeb do
pipe_through :browser
get "/", PageController, :home
end
# Forward all /api requests to the Francis API router.
# Phoenix strips the /api prefix, so Francis routes are relative (e.g. /items).
forward "/api", PhoenixEmbedWeb.FrancisApi
end
josefrichter
Thanks for Francis @filipecabaco ! Always loved Sinatra for prototyping and side projects, so I love Francis by definition.
So much so that I just migrated my personal website https://josefrichter.design to it, and added a simple chat widget there using Francis’ SSE, telegram bot and Mac widget.
Also created francis_template | Hex in the process, because I needed it and the francis_htmx didn’t fit my needs.
filipecabaco
Thank you so much! Really glad to hear that.
I need to rethink fully francis_htmx as it really got way out of hand in complexity which was a sign of bad implementation ![]()
Will check francis_template. I do like the idea as again follows the same ideas as francis - boilerplate reduction ![]()
Popular in Announcing
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









