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


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.

20 Likes

How exactly is Francis different from Plug’s built in Router DSL?

Honestly not much as the idea it’s just to be a boiler plate reducer more than anything. The implementation it’s basically a DSL to simplify the approach with some “sane defaults” focused on reducing as much as we can the boilerplate needed for a quick API

Curious what the community thinks.

Francis is great! Quick API endpoints, prototypes, admin apps…

Related: FrancisHtmx — Francis HTMX v0.2.2

Thank you @filipecabaco !!

PS Does anyone have experience embedding Francis into a Phoenix app?

I think it would be done in the Phoenix router…

defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  <<< phoenix routes >>>

  scope "/francis" do
    forward "/", MyApp.FrancisRouter
  end
end 

Thank you :folded_hands:

honestly never tried it but can experiment with it and add as an example later

1 Like

Ok I did try out and it does work as you said :+1: 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
2 Likes

A new super-power unlocked!

Thanks again for this great library. :heart_eyes:

1 Like

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.

2 Likes

It depends only on francis — no phoenix_html, no heavy view layer.

I’d love to know what you feel is heavy. phoenix_html or phoenix_template are maybe like twice the loc of francis_template and most of the additional loc are the additional form abstraction of phoenix_html. None of that is a heavy dependency.

1 Like

You are right, that was gross overstatement. Fixing! Thank you

1 Like

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 :sweat_smile:

Will check francis_template. I do like the idea as again follows the same ideas as francis - boilerplate reduction :heart:

1 Like