Getting started with Ferra
Ferra is a Rust framework for building HTTP APIs from a single
#[model] source of truth. One model annotation gives you a CRUD
router, an OpenAPI 3.1 spec, a Swagger / Scalar UI, hypermedia
responses, and typed RFC 7807 errors — all derived at compile time.
This page walks you from cargo new to a running, documented API. If
you only have five minutes, read Add Ferra to your project, The
five-line CRUD, and Run it.
Important: the crate is published as ferra-rs
The ferra name on crates.io belongs to an unrelated project, so the
framework’s facade crate is published as ferra-rs. To keep your
Rust code idiomatic, use Cargo’s package alias — declare the
dependency under the local name ferra while pulling ferra-rs from
crates.io:
[dependencies]
ferra = { package = "ferra-rs", version = "0.5" }
The alias is invisible from this point forward. Every use ferra::...
import, every example in this guide, every doc snippet on docs.rs uses
the ferra name — Cargo handles the indirection.
ferra.rs(the domain) andferra-rs(the crate) are two different identifiers. The framework’s official domain isferra.rs— that’s where the documentation site lives and where the closed error-type namespace (https://ferra.rs/errors/<variant>) resolves. The crate name on crates.io isferra-rs(with a hyphen), because the dotlessferraname was already taken. Both are correct and authoritative; do not substitute one for the other.
If you see a plain
ferra = "..."line in older snippets (with nopackage = "ferra-rs"key), that’s pre-rename documentation. Replace it with the form above — the plain line resolves to a different, unrelated crate on crates.io.
Add Ferra to your project
A consumer crate built on Ferra needs one direct framework dependency plus the surrounding async / HTTP / serde stack:
[package]
name = "my-api"
version = "0.1.0"
edition = "2024"
[dependencies]
ferra = { package = "ferra-rs", version = "0.5" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
axum = "0.8"
serde = { version = "1", features = ["derive"] }
That is the entire framework dep surface. ferra-core, ferra-db,
ferra-forge, ferra-http, ferra-openapi, and the surrounding
sea-orm ORM are all reached through the ferra facade — they do
not appear as direct dependencies. Adding them directly fights the
facade’s cohabitation guarantees and is flagged by the framework’s
example dep-shape gate.
The one-line opener
Open every Ferra source file with a single glob over the prelude:
#![allow(unused)]
fn main() {
use ferra::prelude::*;
}
That brings into scope, in one line:
- The
#[derive(FerraModel)]derive — the model annotation. - The typed time newtypes
DateandDateTime. - The typed error type
FerraError. - The runtime state and router primitives
FerraState,ferra_router, and theFoundrybuilder. - The typed-extraction wrappers
FerraJson,FerraPath. - The response envelopes
ItemResponse,CollectionResponseand thePaginationParamsextractor. - Sea-ORM’s entity-derive prelude (
DeriveEntityModel,EntityTrait,ColumnTrait,Uuid, …) reached via the cohabitation re-export. serde::{Deserialize, Serialize}so model structs declare their derive set with no extra imports.
Reaching Sea-ORM through the facade
When you need Sea-ORM types directly — typically the
Database::connect(...) constructor, or the #[sea_orm(...)]
attribute namespace on a model struct — write ferra::sea_orm::...:
#![allow(unused)]
fn main() {
let conn = ferra::sea_orm::Database::connect(&database_url).await?;
}
The prelude glob already brings the sea_orm module name into scope,
so the standard sibling-derive pair #[derive(DeriveEntityModel, FerraModel)] works with no extra use sea_orm::entity::prelude::*;
line.
The five-line CRUD
The smallest meaningful Ferra application — one resource, full CRUD,
auto-generated OpenAPI spec, Scalar docs UI, and HAL hypermedia
responses — fits in this src/main.rs:
use ferra::prelude::*;
mod film {
use ferra::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq,
DeriveEntityModel, FerraModel,
Serialize, Deserialize)]
#[sea_orm(table_name = "films")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub title: String,
pub director: String,
pub year: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
pub use film::Model as Film;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let database_url = std::env::var("DATABASE_URL")?;
let conn = ferra::sea_orm::Database::connect(&database_url).await?;
let app = Foundry::new(conn).mount::<Film>().with_docs().build();
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
What you get on cargo run:
| URL | Behaviour |
|---|---|
GET /films | Paginated list of films with HAL _links. |
GET /films/{id} | Single film + self-link. |
POST /films | Create — validates, returns 201 Created. |
PUT /films/{id} | Update. |
DELETE /films/{id} | Delete. |
GET /docs/openapi.json | Generated OpenAPI 3.1 spec — schema names Film, CreateFilmInput, UpdateFilmInput, FilmCollection, ProblemDetails. |
GET /docs | Interactive Scalar UI rendered from the spec. |
Errors come back as RFC 7807 application/problem+json with typed
type URIs from a closed namespace (https://ferra.rs/errors/<variant>).
The example in
examples/hello-ferra/in the framework repository matches this layout one-to-one and is the canonical reference. If you’re reading this through docs.rs, the same example is published under that name on the Ferra repository.
Run it
You need PostgreSQL reachable at DATABASE_URL. The fastest local
setup uses Podman or Docker:
podman run --rm -d --name pg \
-e POSTGRES_USER=ferra -e POSTGRES_PASSWORD=ferra \
-e POSTGRES_DB=my_api \
-p 5432:5432 \
postgres:16
export DATABASE_URL=postgres://ferra:ferra@localhost:5432/my_api
Apply your schema migrations (Sea-ORM CLI) and start the server:
cargo run
You should see listening on 0.0.0.0:3000 — open http://localhost:3000/docs
in a browser.
MSRV and toolchain
Ferra’s MSRV is Rust 1.88 (edition 2024, resolver v3). let
chains, native async fn in traits, and the rest of the modern Rust
feature surface this guide assumes are stable on 1.88+.
You don’t need a pinned development toolchain in your own project — any stable Rust at or above 1.88 works. The framework itself pins a specific toolchain in its repository for reproducible CI; that pin does not propagate to consumers.
Next steps
- Foundry — the router-assembly facade. Multi-resource
chains, API versioning, the
with_docs_protected(...)auth-gated docs variant. - Ferra Forge — the
#[derive(FerraModel)]derive grammar; the full attribute set on model structs and fields. - Ferra OpenAPI — what the generated spec contains, schema-name derivation, and how SDK generators consume it.
- Custom Handlers — drop down to your own Axum handler when Ferra’s defaults don’t fit a specific endpoint.
- Error Handling — the closed
https://ferra.rs/errors/<variant>namespace, theProblemDetailsshape, and consumer-side branching.