Skip to content

From Dart Frog to Serinus

This guide is for Dart Frog users who want to migrate their applications to Serinus. But first, let's compare the two frameworks.

Dart Frog is a server-side framework for Dart built on top of Shelf, focused on a simple developer experience with file-based routing and middleware-driven dependency injection.

Serinus, on the other hand, is a modular backend framework for Dart that provides built-in structure for routing, dependency injection, hooks, metadata, validation, and typed request body parsing.

Routing

Dart Frog uses file-based routing where each endpoint maps to files in a routes directory. This is straightforward for small projects, but route organization can become harder to manage as the application grows.

Serinus groups routes inside controllers and uses route definitions with explicit HTTP method factories. This keeps route structure centralized and easier to evolve.

dart
import 'package:dart_frog/dart_frog.dart';

Future<Response> onRequest(RequestContext context) async {
  switch (context.request.method) {
    case HttpMethod.get:
      return Response(body: 'Hello, World!');
    default:
      return Response(statusCode: 405);
  }
}
Dart Frog routes are file-based and handlers manually branch on request methods.

Parameterized Routes

In Dart Frog, parameterized endpoints are represented as dynamic route files such as [id].dart.

In Serinus, parameterized routes are defined directly in the same controller using path parameters.

dart
import 'package:dart_frog/dart_frog.dart';

// routes/posts/index.dart
Future<Response> onRequest(RequestContext context) async {
  return Response(body: 'post list');
}

// routes/posts/[id].dart
Response onRequest(RequestContext context, String id) {
  return Response(body: 'post id: $id');
}
Dart Frog parameterized routes require separate files for each dynamic segment.

Dependency Injection

Dart Frog supports dependency injection through middleware and dependency access with context.read<T>() and their values are created per request and not shared across requests unless you use a global variable or a singleton pattern.

Serinus supports dependency injection through Providers declared on a Module, with dependencies consumed via context.use<T>().

dart
import 'package:dart_frog/dart_frog.dart';

Handler middleware(Handler handler) {
  return handler.use(provider<String>((context) => 'Welcome to Dart Frog!'));
}

Future<Response> onRequest(RequestContext context) async {
  final greeting = context.read<String>();
  return Response(body: greeting);
}
Dart Frog commonly injects dependencies via middleware layers.

Hooks and Metadata

Serinus includes hooks and route metadata for request-lifecycle logic and route-level behavior tagging.

Dart Frog does not provide an equivalent built-in metadata system for route behavior declarations.

dart
// No built-in hook + metadata system.
// Similar behavior is usually modeled 
// with middleware and custom conventions.
Dart Frog can implement similar behavior, but not through a dedicated metadata API.

Interoperability with Shelf

Dart Frog is built on Shelf, so Shelf middleware and ecosystem packages are naturally available.

Serinus is not built on Shelf, but supports interoperability with Shelf middleware to ease migration and reuse existing tooling.

dart
import 'package:shelf/shelf.dart';

Handler middleware(Handler handler) {
  return handler.use((inner) => logRequests()(inner));
}
Dart Frog uses Shelf middleware directly because it is Shelf-based.

Validation

Dart Frog does not include built-in request validation primitives, so validation is generally implemented with custom code or third-party packages.

Serinus provides built-in validation powered by Acanthis, allowing validation of request data before it reaches handlers.

dart
import 'dart:convert';
import 'package:dart_frog/dart_frog.dart';

Future<Response> onRequest(RequestContext context) async {
  final payload = await context.request.body();
  final data = jsonDecode(payload) as Map<String, dynamic>;
  final name = data['name'] as String?;
  if (name == null || name.isEmpty) {
    return Response.json(statusCode: 400, body: {'error': 'Name is required'});
  }
  return Response.json(body: {'message': 'User $name created'});
}
Dart Frog validation is typically manual or delegated to third-party code.

Typed Responses and Body Parsing

Dart Frog handlers return Response objects and body parsing is handled manually within the handler.

Serinus supports typed request body parsing and typed handler signatures for clearer contracts.

dart
import 'dart:convert';
import 'package:dart_frog/dart_frog.dart';

Future<Response> onRequest(RequestContext context) async {
  final payload = await context.request.body();
  final data = jsonDecode(payload) as Map<String, dynamic>;
  return Response.json(body: {'message': 'Hello ${data['name']}'});
}
Dart Frog requires manual body decoding and shaping of response payloads.

INFO

Typed body parsing code generation requires serinus_cli with serinus generate models.

Conclusion

Both Dart Frog and Serinus are strong options for building backend services in Dart.

If you prefer a file-first approach with close Shelf alignment, Dart Frog is a solid choice. If you prefer a structured, modular architecture with built-in hooks, metadata, validation, and typed body parsing, Serinus provides those capabilities out of the box.

© 2025 Francesco Vallone. Built with 💙 and Dart 🎯 | One of the 🐤 of Avesbox.