Clean architecture

Shelf

A long tutorial. Build a hybrid Serverpod API and a hybrid Flutter client, one file at a time, then instrument both.

clean-architecturetutorialserverpodflutter

This is the long tutorial. You build one app, Shelf, in the order you would type it. A shelf holds books. You can save a book, save a shelf, place a book on a shelf, and mark a book as a favourite. Placing a book is one use case that needs both features. Favouriting a book needs only the book repository.

Both sides are hybrid.

On the server, domain/, application/, and infra/ are horizontal. The name book repeats inside each. presentation/book/ holds the endpoint, the spy wire types, and wire_mappers.dart together. There is no shared presentation/mappers/wire_mappers.dart. app/ is the only place that constructs a repository implementation. A use case never moves into the presentation folder just because the endpoint lives there.

On Flutter, domain/, application/, and data/ are horizontal. Screens live in presentation/features/. ui/ does not import Riverpod. Providers in app/providers.dart are the composition root. Riverpod 3 is written by hand. Routing is Beamer.

Feature-first is the wrong tree for this server. A workflow that touches two aggregates has no home: it is not only features/book and not only features/shelf. Layer-first keeps that home, and it is what a large existing server may already use. The part that does not earn its cost on a new project is one shared wire-mapper file that every feature appends to. Hybrid keeps the inner layers shared and puts each mapper next to its endpoint.

The running code is examples/tutorial.

What you are building

Shelf is a reading shelf for one person on one machine. There are no accounts. Every chapter is about a layer boundary.

A book has a title, an author, a reading status (unread, reading, or read), and a favourite flag. A shelf has a name and a capacity from 1 to 500. A book sits on at most one shelf. A shelf refuses another book when it is full. A favourite does not move the book. You open a shelf, or Favourites, and see those books.

You finish books on the server, then on Flutter, so you can save a book in the app before shelves exist. Shelves repeat the same path in shorter chapters. Placing a book is the chapter that uses both repositories.

Technologies

Piece Choice
Language Dart, everywhere a type is not a Postgres row or an API contract
Server Serverpod 4
Database PostgreSQL, through the Serverpod ORM
Persistence models infra/models/*.spy.yaml, serverOnly: true
API models presentation/**/*.spy.yml
Server tests package:test, fakes for use cases, withServerpod for the endpoint
Client Flutter
Client state Riverpod 3, hand-written providers, no riverpod_annotation
Client models dart_mappable on the domain entities
Router Beamer
Instrumentation talaria_serverpod, then talaria_flutter

Spy files exist only where Serverpod has to generate a table or a client type. Entities, value objects, repository interfaces, use cases, commands, and both kinds of mapper are Dart. Domain and application never import lib/src/generated/ or package:shelf_client.

The API in this sample listens on port 8280. Insights is 8281, the web server is 8282, and Postgres is 8290. The test database is 9290. Those ports stay off 8080 and 8090.

The server map

text
lib/src/domain/book/
lib/src/domain/shelf/
lib/src/domain/shared/exceptions/
lib/src/application/book/
lib/src/application/shelf/
lib/src/application/ports/
lib/src/infra/models/          stored_book.spy.yaml, stored_shelf.spy.yaml
lib/src/infra/book/
lib/src/infra/shelf/
lib/src/presentation/book/     endpoint, spy wire types, wire_mappers.dart
lib/src/presentation/shelf/
lib/src/app/                   repositories.dart, use_cases.dart, di.dart
lib/src/bootstrap/talaria_monitoring.dart

The Flutter map

text
lib/domain/book/
lib/domain/shelf/
lib/application/book/
lib/application/shelf/
lib/data/                     Serverpod repositories and ProtocolMappers
lib/presentation/features/books/
lib/presentation/features/shelves/
lib/presentation/features/favourites/
lib/presentation/router/      Beamer
lib/ui/                       tiles, shelf_theme.dart, shelf_frame.dart, no Riverpod
lib/app/providers.dart
lib/bootstrap/talaria_monitoring.dart

The Flutter domain is written again. The client package cannot import the server's domain. The rules are the same. The ids are not: the server assigns the id that the client stores.

Read it in this order

Server, books, until it runs

  1. Project
  2. Failures
  3. Book title
  4. Reading status
  5. Book
  6. Book repository
  7. Ports
  8. Save a book
  9. List books
  10. Book tests
  11. Stored book
  12. Book mapper
  13. Book repository implementation
  14. Book wire types
  15. Book endpoint
  16. Composition root
  17. Integration test

Flutter, books, so you can demo

  1. Flutter project
  2. Flutter domain
  3. Flutter application
  4. Data
  5. Providers
  6. Books screen
  7. Run the app

Shelves, then both features

  1. Shelves on the server
  2. Shelves on Flutter
  3. Place a book on a shelf
  4. The place control
  5. Favourite a book
  6. Favourite on Flutter

Talaria, after the app runs

  1. Server instrumentation
  2. Flutter instrumentation
  3. Where to go next

The short courses are still there if you want to diff the three layouts on a notes API. This tutorial is the one to follow when you are starting a Serverpod project.