Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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) and ferra-rs (the crate) are two different identifiers. The framework’s official domain is ferra.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 is ferra-rs (with a hyphen), because the dotless ferra name 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 no package = "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 Date and DateTime.
  • The typed error type FerraError.
  • The runtime state and router primitives FerraState, ferra_router, and the Foundry builder.
  • The typed-extraction wrappers FerraJson, FerraPath.
  • The response envelopes ItemResponse, CollectionResponse and the PaginationParams extractor.
  • 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:

URLBehaviour
GET /filmsPaginated list of films with HAL _links.
GET /films/{id}Single film + self-link.
POST /filmsCreate — validates, returns 201 Created.
PUT /films/{id}Update.
DELETE /films/{id}Delete.
GET /docs/openapi.jsonGenerated OpenAPI 3.1 spec — schema names Film, CreateFilmInput, UpdateFilmInput, FilmCollection, ProblemDetails.
GET /docsInteractive 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, the ProblemDetails shape, and consumer-side branching.