Skip to main content

Server & Web Routes

The server is where everything comes together. We use Shelf to handle HTTP requests and Shelf Router to map URLs to specific handler functions.

Bootstrap the Shelf server

The entry point of our application initializes the database, storage, and template engine before starting the server. We also use Pipeline to add middleware, such as logRequests and our custom contextual_shelf middleware for structured logging.

Future<HttpServer> runServer({String host = '0.0.0.0', int port = 8080}) async {
final logger = buildLogger();
final httpLogger = buildHttpLogger(logger);

final storage = StorageService();
await storage.init();

final database = AppDatabase();
await database.init();

final templates = TemplateRenderer(
FileSystemRoot(AppPaths.templatesDir, throwOnMissing: true),
sharedData: {'app_name': 'Movie Catalog', 'year': DateTime.now().year},
);

final app = MovieCatalogApp(
database: database,
storage: storage,
templates: templates,
logger: logger,
);

final handler = Pipeline()
.addMiddleware(requestIdMiddleware())
.addMiddleware(httpLogger.middleware)
.addHandler(app.buildHandler());

final server = await serve(handler, host, port);
logger.info('Server running', Context({'host': host, 'port': port}));
return server;
}

Router map

We divide our routes into two main sections: Web (for HTML pages) and API (for JSON data). This keeps the application organized and easy to maintain.

      ..get('/', _index)
..get('/genres', _genresIndex)
..get('/genres/<id|[0-9]+>', _showGenre)
..get('/movies/new', _newMovie)
..post('/movies', _createMovie)
..get('/movies/<id|[0-9]+>', _showMovie)
..get('/movies/<id|[0-9]+>/edit', _editMovie)
..post('/movies/<id|[0-9]+>/edit', _updateMovie)
..get('/movies/<id|[0-9]+>/delete', _confirmDeleteMovie)
..post('/movies/<id|[0-9]+>/delete', _deleteMovie)

Movie handlers

Handlers are responsible for processing requests, interacting with the database, and returning a response.

Listing Movies

The index handler fetches movies from the database, including their associated genres, and renders them using a template.

    final movies = await database.dataSource
.query<$Movie>()
.withRelation('genre')
.orderBy('createdAt', descending: true)
.get();

final viewModels = movies.map(_movieViewModel).toList();

final html = await templates.render('movies/index.liquid', {
'movies': viewModels,
});

return _html(html);

Creating a Movie

Creating a movie is a multi-step process. We've broken it down to show how each part works:

We use shelf_multipart to handle file uploads (the movie poster) alongside regular form fields.

    final form = request.formData();
if (form == null) {
return Response(HttpStatus.badRequest, body: 'Expected form data');
}

final fields = <String, String>{};
List<int>? posterBytes;
String? posterName;

await for (final formData in form.formData) {
if (formData.name == 'poster' && formData.filename != null) {
posterName = formData.filename;
posterBytes = await formData.part.readBytes();
} else {
fields[formData.name] = await formData.part.readString();
}
}

Updating a Movie

Updating follows a similar pattern, using MovieUpdateDto to apply changes.

    final repo = database.dataSource.repo<$Movie>();
final movie = await repo.update(
MovieUpdateDto(
title: fields['title']!.trim(),
releaseYear: int.parse(fields['releaseYear']!),
summary: _nullIfEmpty(fields['summary']),
genreId: _parseOptionalInt(fields['genreId']),
),
where: {'id': movieId},
);

Genre handlers

Genres are simpler as they are mostly read-only in this example.

    final genres = await database.dataSource
.query<$Genre>()
.orderBy('name')
.get();

final html = await templates.render('genres/index.liquid', {
'genres': genres.map(_genreViewModel).toList(),
});

return _html(html);

Structured logging

Structured logging (via Contextual) is essential for debugging and observability. Instead of plain text logs, we emit JSON logs with attached context like request IDs and user information.

We configure a logger that outputs structured JSON.

Logger buildLogger() {
final logger = Logger()
..environment('development')
..addChannel(
'console',
ConsoleLogDriver(),
formatter: PrettyLogFormatter(),
);

return logger;
}