Nova : le web framework BEAM qui te traite comme un citoyen de première classe

Quand tu veux du web sur BEAM sans les abstractions Elixir

Le BEAM a deux web frameworks principaux. Phoenix et Nova. Phoenix est très bien mais fat et plein de feature dont tu as peut etre pas besoin pour de simples services rest. En plus si tu as du legacy en Erlang et que t’a pas envie de te taper la migration, Nova résout ce problème. Et il le fait bien.

C’est là mes définitions et si elles vous déplaisent, cassez-vous je suis pas là pour discuter lexique.

  • Nova: web framework pour le BEAM, supporte Erlang, Elixir et LFE. MVC, plugin pipeline, WebSockets, PubSub. Construit sur Cowboy.
  • BEAM: la machine virtuelle d’Erlang. Le truc qui fait tourner Erlang, Elixir, LFE, et Nova.
  • Cowboy: serveur HTTP Erlang, léger et rapide. Nova l’utilise comme backend HTTP.
  • OTP: Open Telecom Platform — le framework de patterns pour Erlang. Supervision trees, GenServer, releases. Nova est OTP-native.
  • Plugin pipeline: middleware composable avec pre/post request hooks. CORS, CSRF, auth — tout est un plugin.
  • Kura: couche de base de données pour Erlang, inspirée par Ecto. Schemas, changesets, migrations, queries.
  • nova_resilience: module de résilience pour Nova. Circuit breakers, bulkheads, health checks, graceful shutdown.
  • ErlyDTL: moteur de templates pour Erlang, basé sur Django Template Language.
  • Arizona: module de live views pour Nova, équivalent de LiveView pour Phoenix.
  • Hikyaku: module d’email transactionnel pour Nova.

Le problème c’est simple. Si tu as une codebase Erlang existante et que tu veux ajouter une API REST, tu fais quoi ? Et si tu as veux une codebase plus simple et pas toute la lourdeur de Phoenix?

Tu finis avec Cowboy brut. Ou tu essayes de faire tourner Phoenix dans un mixte Erlang/Elixir. C’est possible mais un poil foireux.

Avec Nova pas besoin de mixte Erlang/Elixir. Le framework est plus lean, ainsi ta codebase sera aussi bien plus concise et simples à comprendre.

Client (HTTP/WebSocket)
    │
    └── Cowboy (serveur HTTP)
        │
        ├── Nova Router
        │   ├── Path parameters, prefixes
        │   ├── Per-route security
        │   └── Environment-based routing
        │
        ├── Plugin Pipeline (pre_request)
        │   ├── CORS
        │   ├── CSRF
        │   ├── Correlation ID
        │   └── Custom plugins
        │
        ├── Security Middleware
        │   └── Authentication/Authorization
        │
        ├── Controller
        │   ├── {json, Map}
        │   ├── {ok, Variables}
        │   ├── {status, Code}
        │   ├── {redirect, Path}
        │   └── {sendfile, ...}
        │
        ├── Plugin Pipeline (post_request)
        │
        └── Response
Request → Cowboy → Nova Router → Plugins (pre) → Security → Controller → Plugins (post) → Response

C’est du MVC classique, adapté au Erlang. Rien de révolutionnaire ici.

Nova Framework
├── nova              (core framework)
├── rebar3_nova       (scaffolding & generators)
├── nova_test         (testing utilities)
├── kura              (database layer — Ecto-inspired)
├── rebar3_kura       (migration generator)
├── opentelemetry_nova (tracing & metrics)
├── nova_resilience   (circuit breakers, bulkheads, health)
├── nova_websocket    (WebSocket behaviour)
├── nova_pubsub       (distributed pub/sub)
├── Arizona           (live views)
└── Hikyaku           (email transactionnel)
0routes(_Environment) ->
1    [#{prefix => "/api",
2       security => false,
3       routes => [
4           {"/", {my_app_main_controller, index}},
5           {"/items", {my_app_main_controller, create}}
6       ]}].

Path parameters, prefixes, per-route security, environment-based routing. C’est du MVC classique, adapté au Erlang.

0-module(my_app_main_controller).
1-export([index/1, create/1]).
2
3index(#{method := <<"GET">>} = _Req) ->
4    {json, #{message => <<"Hello from Nova!">>}}.
5
6create(#{method := <<"POST">>, json := Body} = _Req) ->
7    {json, #{status => <<"created">>, data => Body}}.

Les controllers sont des fonctions Erlang qui prennent un request map et retournent des tuples. {json, Map}, {ok, Variables}, {status, Code}, {redirect, Path}, {sendfile, ...}.

C’est simple. C’est direct. C’est Erlang.

0%% Pre-request plugin
1pre_request(Req) ->
2    %% Log, auth, CORS, whatever
3    {ok, Req}.
4
5%% Post-request plugin
6post_request(Req, Res) ->
7    %% Add headers, log response, whatever
8    {ok, Res}.

Des hooks composable pour auth, CORS, logging, et custom concerns. Configurable par route group. Plus flexible que Plug.

 0%% WebSocket handler
 1-module(my_ws_handler).
 2-behaviour(nova_websocket).
 3
 4init(Req, State) ->
 5    {ok, State}.
 6
 7handle(Data, State) ->
 8    %% Broadcast to all connected clients
 9    nova_pubsub:broadcast("chat", {message, Data}),
10    {ok, State}.
11
12terminate(_Reason, _State) ->
13    ok.

WebSockets natifs via nova_websocket. PubSub distribué via OTP’s pg module. Marche sur des nodes clusterés. Pas besoin de Redis.

Putain, c’est beau.

0<!-- templates/index.html -->
1<html>
2<body>
3    <h1>{{ title }}</h1>
4    <p>{{ content }}</p>
5</body>
6</html>

ErlyDTL — Django Template Language pour Erlang. Hot reload via rebar3 nova serve.

 0%% Schema
 1-module(pets).
 2-include_lib("kura/include/schema.hrl").
 3
 4-export([schema/0]).
 5
 6schema() ->
 7    #schema{
 8        name = pets,
 9        fields = [
10            #field{name = name, type = string},
11            #field{name = species, type = string},
12            #field{name = owner_id, type = integer, references = users}
13        ]
14    }.
15
16%% Changeset
17changeset(Pet, Params) ->
18    kura:cast(Pet, Params, [name, species, owner_id])
19    |> kura:validate_required([name, species])
20    |> kura:validate_length(:name, min: 1, max: 100).

Kura est l’inspiration Ecto pour Erlang. Schemas, changesets, migrations, queries, associations. Si tu connais Ecto, Kura te familier.

C’est LA feature qui distingue Nova de Phoenix. nova_resilience fournit :

  • Health endpoints/health, /ready, /live pour Kubernetes probes
  • Startup gating — traffic held jusqu’à ce que les dépendances critiques soient healthy
  • Circuit breakers — arrêter d’appeler les dépendances qui échouent
  • Bulkheads — limiter les requêtes concurrentes par dépendance
  • Retry — retry configurable avec exponential backoff et jitter
  • Deadline propagation — timeouts par request via headers
  • Graceful shutdown — teardown ordonné avec drain et coordination LB
 0{nova_resilience, [
 1    {dependencies, [
 2        #{name => primary_db,
 3          type => database,
 4          adapter => pgo,
 5          critical => true,
 6          shutdown_priority => 1,
 7          breaker => #{
 8              failure_threshold => 5,
 9              wait_duration => 30000
10          },
11          bulkhead => #{
12              max_concurrent => 100
13          }
14        },
15        #{name => kafka,
16          type => kafka,
17          adapter => brod,
18          critical => false,
19          shutdown_priority => 10
20        }
21    ]},
22    {health_check_interval, 10000},
23    {vm_checks, true},
24    {gate_enabled, true},
25    {gate_timeout, 30000},
26    {shutdown_delay, 5000},
27    {shutdown_drain_timeout, 15000}
28]}.

Et c’est tout. /health, /ready, /live sont exposés. Circuit breakers, bulkheads, graceful shutdown — tout est configuré. Pas de code manuel.

Tu vois le problème avec Phoenix ? Il n’a rien de comparable intégré. Tu dois configurer 5 libraries externes pour avoir la même chose.

0%% Monter une app sous un prefix
1{"/admin", {admin_app, []}},
2{"/api", {api_app, []}}

Comme des Rails engines. Tu peux monter des Nova apps sous des path prefixes. Utile pour les monolithes modulaires.

0# Release avec hot code upgrade
1$ rebar3 release
2$ _build/default/rel/my_app/bin/my_app upgrade "2"

Nova est OTP-native. Hot code upgrades, rolling upgrades, releases: tout est supporté nativement. Pas besoin d’outils externes.

Critère Nova Phoenix
Langage Erlang (et Elixir, LFE) Elixir uniquement
HTTP Server Cowboy Cowboy ou Bandit
OTP-native Oui — supervision trees, releases Oui — mais plus d’abstraction
Plugin pipeline Oui — pre/post hooks Non — Plug middleware
WebSockets nova_websocket behaviour Phoenix Channels
PubSub OTP’s pg distribué Phoenix PubSub
Live views Arizona Phoenix LiveView
Database Kura (Ecto-inspired) Ecto (plus mature)
Résilience nova_resilience (circuit breakers, bulkheads) Pas intégré (utiliser des libs externes)
Health checks /health, /ready, /live natifs Pas intégré
Graceful shutdown Ordre configurable avec drain Pas intégré
Templates ErlyDTL (Django-style) HEEx (HTML + Embedded Elixir)
Community ~300 stars, petite ~22k stars, massive
Documentation Nova Book, guides Extensive, beaucoup de tutos
Production use Limité Massif (Discord, etc.)
Multi-langage Erlang + Elixir + LFE Elixir uniquement
License Apache 2.0 Apache 2.0

Le choix Nova vs Phoenix, c’est un choix de langage et de niveau d’abstraction.

  • Si tu écris du Erlang et que tu veux moins d’abstraction → Nova
  • Si tu écris du Elixir et que tu veux le meilleur écosystème → Phoenix
  • Si tu veux de la résilience intégrée (circuit breakers, health checks) → Nova
  • Si tu veux du LiveView mature → Phoenix
  • Si tu veux du multi-langage BEAM → Nova

Les deux sont d’excellent frameworks. Tout est trade-off.

Si tu as déjà du Erlang en prod et que tu veux ajouter une API REST, Nova est le choix naturel. Pas besoin de migrer vers Elixir. Pas besoin de mixte Erlang/Elixir. juste rebar3 new nova my_api.

Si tu veux des circuit breakers, des health checks pour Kubernetes, du graceful shutdown — nova_resilience fait tout. Pas besoin de 5 libraries externes.

Avec les sub-applications, tu peux monter des Nova apps sous des path prefixes. /admin = une app, /api = une autre. Architecture modulaire sans microservices.

PubSub distribué via OTP’s pg. Pas de Redis. Pas de Kafka. Juste du BEAM natif. Les messages passent entre les nodes du cluster automatiquement.

Si tu as besoin de déployer sans downtime, Nova est OTP-native. Hot code upgrades, rolling upgrades — tout est supporté nativement.

Risque Impact Mitigation
Community petite Modéré Nova est jeune mais croît. Cowboy est battle-tested.
Documentation limitée Modéré Le Nova Book existe. Les guides sont là.
Kura moins mature qu’Ecto Faible-Modéré Kura couvre les use cases courants. Pour les edge cases, utiliser Erlang directement.
Production use limité Modéré Nova tourne en prod. Pas autant que Phoenix, mais ça marche.
ErlyDTL old-school Faible ErlyDTL marche. Arizona est plus moderne pour les live views.
Pas de LiveView mature Modéré Arizona existe. Pour du live view complexe, Phoenix est mieux.
Multi-langage = complexité Faible C’est un feature, pas un bug. Mais faut gérer les 3 langages.
  • Erlang/OTP 23+ (28+ pour Arizona)
  • Rebar3 — build tool Erlang
  • Docker — pour PostgreSQL (optionnel)
  • OS: Linux, macOS, Windows
  • Taille: ~10 MB (core framework)
  • Memory: 20 MB par container (si utilisé avec Incus)

Nova utilise toute la puissance de Erlang/BEAM avec moins d’abstranction et de “boilerplate” que Phoenix. Il brille dans de petit services simples et ultra-résilients.

Et si tu écris du Erlang, c’est le seul web framework mature qui te traite comme un citoyen de première classe.

See ya space-cowboy!