We build a production-ready containerized RESTful API server application using Axum, Tokio, Tower, Serde, Toasty ORM, Garde, Utoipa with Docker and PostgreSQL.
- Hyper, Axum, Tokio and Tower: The most prominent HTTP server ecosystem at the time of writing.
- Toasty ORM: The most promising Object-Relational Mapper (ORM), built by the creators of Tokio and Axum.
- Serde and Utoipa: The most prominent serialization framework and OpenAPI 3.1 specification generator.
- Garde: The most promising and most feature-rich validation library in Rust at the time of writing.
Database Design
To keep this series simple, we use only a single database table named books.
| Column Name | Datatype | Not Null | Primary Key |
|---|---|---|---|
| created_at | TIMESTAMPTZ | ✅ | |
| updated_at | TIMESTAMPTZ | ✅ | |
| id | UUID | ✅ | ✅ |
| published_date | DATE | ✅ | |
| status | SMALLINT | ✅ | |
| title | TEXT | ✅ | |
| description | TEXT | ||
| image_url | TEXT |
- For high-traffic systems with very large PostgreSQL tables that containing millions/billions of rows, arranging fixed-width columns by decreasing alignment requirements can reduce tuple alignment padding; potentially minimize row/ storage size. This technique is called “Column Tetris”.
- For this optimization, order fixed-width table columns by decreasing alignment requirements.
- 8-byte alignment types:
bigint,bigserial,double precision/float8,timestamp,timestamptz,time,interval - 4-byte alignment types:
integer,serial,real/float4,uuid,date - 2-byte alignment types:
smallint,smallserial - 1-byte alignment types:
boolean - Variable-width types (at last):
numeric,text,character varying/varchar,bytea
- 8-byte alignment types:
- However, it’s ok to follow a more readable column format, when your table schema changes frequently.
Endpoints
| Name | HTTP Method | Route |
|---|---|---|
| List Books | GET | /v1/books |
| Create Book | POST | /v1/books |
| Read Book | GET | /v1/books/{id} |
| Update Book | PUT | /v1/books/{id} |
| Delete Book | DELETE | /v1/books/{id} |
| Health | GET | /livez |
Request (POST/PUT)
{
"title": "Harry Potter and the Deathly Hallows",
"description": "It is the seventh and final novel in the Harry Potter series",
"image_url": "https://upload.wikimedia.org/wikipedia/en/a/a9/Harry_Potter_and_the_Deathly_Hallows.jpg",
"published_date": "2007-07-21",
"status": "verified"
}Response (GET/POST/PUT)
{
"created_at": "2027-01-01T00:00:00.123456Z",
"updated_at": "2027-01-01T00:00:00.123456Z",
"id": "01bbbbbb-bbbb-7bbb-8bbb-bbbbbbbbbbbb",
"published_date": "2007-07-21",
"status": "verified",
"title": "Harry Potter and the Deathly Hallows",
"description": "It is the seventh and final novel in the Harry Potter series",
"image_url": "https://upload.wikimedia.org/wikipedia/en/a/a9/Harry_Potter_and_the_Deathly_Hallows.jpg"
}The list endpoint returns an array of above response JSON.
Form Validation
{
"errors": {
"title": "Must be at least 1 character long",
"image_url": "Must be a valid URL"
}
}Project Structure
rest_api_workspace
├── crates
│ ├── book_service
│ │ ├── src
│ │ │ ├── bin
│ │ │ │ ├── app.rs
│ │ │ │ ├── migration.rs
│ │ │ │ └── apidoc.rs
│ │ │ ├── app
│ │ │ │ ├── mod.rs
│ │ │ │ ├── book
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ ├── handler.rs
│ │ │ │ │ └── payload.rs
│ │ │ │ └── shared
│ │ │ │ ├── mod.rs
│ │ │ │ ├── pagination.rs
│ │ │ │ └── validation.rs
│ │ │ ├── models
│ │ │ │ ├── mod.rs
│ │ │ │ └── book.rs
│ │ │ ├── config.rs
│ │ │ ├── errors.rs
│ │ │ ├── state.rs
│ │ │ ├── routes.rs
│ │ │ ├── openapi.rs
│ │ │ └── lib.rs
│ │ ├── Toasty.toml
│ │ ├── toasty
│ │ │ ├── history.toml
│ │ │ ├── migrations
│ │ │ │ └── 0000_migration.sql
│ │ │ └── snapshots
│ │ │ └── 0000_snapshot.toml
│ │ ├── Cargo.toml
│ │ ├── openapi.yaml
│ │ ├── compose.yml
│ │ ├── prod.Dockerfile
│ │ ├── Dockerfile
│ │ └── justfile
│ └── README.md
├── Cargo.lock
├── Cargo.toml
├── rustfmt.toml
├── LICENSE
├── README.md
└── justfileContainerization Environment
| Environment | Rust Image Type | Rust Image Size | Postgres Image Type | Postgres Image Size |
|---|---|---|---|---|
| Development | rust:1.97-slim | ~ 900 MB | postgres:18-alpine | ~ 300MB |
| Production | distroless/static-debian13:nonroot | ~ 15 MB |