Building a Containerized RESTful API

· Learning Rust

3 min read Original article ↗

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 NameDatatypeNot NullPrimary Key
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ
idUUID
published_dateDATE
statusSMALLINT
titleTEXT
descriptionTEXT
image_urlTEXT
  • 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
  • However, it’s ok to follow a more readable column format, when your table schema changes frequently.

Endpoints

NameHTTP MethodRoute
List BooksGET/v1/books
Create BookPOST/v1/books
Read BookGET/v1/books/{id}
Update BookPUT/v1/books/{id}
Delete BookDELETE/v1/books/{id}
HealthGET/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
└── justfile

Containerization Environment

EnvironmentRust Image TypeRust Image SizePostgres Image TypePostgres Image Size
Developmentrust:1.97-slim~ 900 MBpostgres:18-alpine~ 300MB
Productiondistroless/static-debian13:nonroot~ 15 MB