dart-frog

SKILL.md

Dart Frog Framework Guide

Applies to: Dart Frog 1.x, Dart 3.x, REST APIs, Full-Stack Dart, Serverless Functions

Overview

Dart Frog is a fast, minimalistic backend framework for Dart built on top of Shelf. It provides file-based routing (similar to Next.js), built-in middleware support, dependency injection via providers, hot reload during development, and easy deployment with Docker. Dart Frog pairs naturally with Flutter for full-stack Dart applications.

Key Features

  • File-based routing: route structure mirrors the filesystem
  • Middleware cascades: _middleware.dart files apply to all routes in their directory subtree
  • Dependency injection: provider<T>() and context.read<T>() for clean service access
  • Hot reload: dart_frog dev watches for changes automatically
  • Production builds: compiles to a self-contained server binary
  • Docker-ready: ships with a Dockerfile scaffold

Guardrails

Project Organization

  • Place all route handlers under routes/
  • Place business logic, models, repositories, and services under lib/src/
  • Export a barrel file at lib/<project_name>.dart
  • Keep route files thin: delegate to services, never embed business logic in handlers
  • Mirror test structure under test/routes/ to match routes/ layout

Code Style

  • Run dart format . before every commit
  • Run dart analyze and fix all warnings before committing
  • Use very_good_analysis lint rules (or lints package at minimum)
  • Exclude generated files (*.g.dart, *.freezed.dart) from analysis

Error Handling

  • Define custom exception classes extending a base AppException
  • Use a global error-handler middleware to catch all exceptions and return JSON
  • Never expose stack traces in production responses
  • Always return structured JSON error bodies: {"error": "<message>"}
  • Use dart:io HttpStatus constants instead of magic status code numbers

Security

  • Validate all request body fields before processing
  • Use parameterized queries for all database access
  • Never hardcode secrets; read from environment variables via a Config class
  • Set CORS allowed origins to specific domains in production (never *)
  • Verify JWT signatures with a secret loaded from config, not inline strings

Project Structure

myapp/
routes/
  _middleware.dart          # Global middleware (logging, CORS, error handler, DI)
  index.dart                # GET /
  health.dart               # GET /health
  api/
    _middleware.dart        # JSON content-type header
    v1/
      _middleware.dart      # Auth provider
      users/
        _middleware.dart    # Require authentication
        index.dart          # GET|POST /api/v1/users
        [id].dart           # GET|PUT|DELETE /api/v1/users/:id
      posts/
        index.dart
        [id].dart
    auth/
      login.dart            # POST /api/v1/auth/login
      register.dart         # POST /api/v1/auth/register
  ws.dart                   # WebSocket endpoint
lib/
  myapp.dart                # Barrel export
  src/
    models/
      user.dart
    repositories/
      user_repository.dart
    services/
      user_service.dart
    middleware/
      auth_provider.dart
    exceptions.dart
test/
  routes/
    api/v1/users/
      index_test.dart
pubspec.yaml
Dockerfile

File-Based Routing

Route-to-File Mapping

URL Path File
/ routes/index.dart
/health routes/health.dart
/api/v1/users routes/api/v1/users/index.dart
/api/v1/users/:id routes/api/v1/users/[id].dart
/api/v1/users/:uid/posts/:pid routes/api/v1/users/[uid]/posts/[pid].dart

Handler Signature

Every route file must export a top-level onRequest function:

// Synchronous handler (no async work)
Response onRequest(RequestContext context) { ... }

// Async handler
Future<Response> onRequest(RequestContext context) async { ... }

// Dynamic route — parameters are positional String arguments
Future<Response> onRequest(RequestContext context, String id) async { ... }

// Nested dynamic route — one String per segment
Future<Response> onRequest(
  RequestContext context,
  String userId,
  String postId,
) async { ... }

Method Routing

Use a switch on context.request.method to dispatch by HTTP verb:

Future<Response> onRequest(RequestContext context) async {
  return switch (context.request.method) {
    HttpMethod.get => _handleGet(context),
    HttpMethod.post => _handlePost(context),
    _ => Future.value(Response(statusCode: HttpStatus.methodNotAllowed)),
  };
}

Always return 405 Method Not Allowed for unsupported verbs.

Request Context

Reading the Request

// Query parameters
final page = int.tryParse(
  context.request.uri.queryParameters['page'] ?? '1',
) ?? 1;

// JSON body
final body = await context.request.json() as Map<String, dynamic>;

// Headers
final auth = context.request.headers['Authorization'];

Dependency Access

Read any provider-registered dependency:

final userService = context.read<UserService>();
final config = context.read<Config>();
final currentUser = context.read<User?>(); // nullable for optional auth

Building Responses

// JSON with default 200
Response.json({'message': 'OK'});

// JSON with explicit status
Response.json(user.toJson(), statusCode: HttpStatus.created);

// No content
Response(statusCode: HttpStatus.noContent);

// Custom headers
response.copyWith(headers: {...response.headers, 'X-Custom': 'value'});

Middleware

Middleware files are named _middleware.dart and apply to every route in the same directory and all subdirectories. They execute from outermost to innermost (root first).

Anatomy

Handler middleware(Handler handler) {
  return handler
      .use(someMiddleware())
      .use(provider<SomeType>((context) => SomeType()));
}

Middleware Cascade Order

routes/_middleware.dart           -> runs first (global)
routes/api/_middleware.dart       -> runs second
routes/api/v1/_middleware.dart    -> runs third (e.g., auth provider)
routes/api/v1/users/_middleware.dart -> runs last (e.g., require auth)
routes/api/v1/users/index.dart   -> handler

Writing Custom Middleware

A Middleware is a function Handler Function(Handler):

Middleware myMiddleware() {
  return (handler) {
    return (context) async {
      // Before handler
      final response = await handler(context);
      // After handler
      return response;
    };
  };
}

Common Middleware Patterns

  • Request logger: time the request, log method/path/status/duration
  • CORS: add Access-Control-Allow-* headers, handle OPTIONS preflight
  • Error handler: wrap handler in try/catch, map exceptions to HTTP status codes
  • JSON content-type: add Content-Type: application/json to responses
  • Auth provider: parse JWT from Authorization header, provide User?
  • Require auth: check context.read<User?>(), return 401 if null

Dependency Injection

Dart Frog uses the provider<T>() middleware to register dependencies:

Handler middleware(Handler handler) {
  return handler
      .use(provider<Config>((_) => Config.fromEnvironment()))
      .use(provider<Database>((ctx) => Database(ctx.read<Config>().dbUrl)))
      .use(provider<UserRepository>((ctx) =>
          UserRepositoryImpl(ctx.read<Database>())))
      .use(provider<UserService>((ctx) => UserService(
          ctx.read<UserRepository>(),
          ctx.read<Config>(),
      )));
}

Rules

  • Register providers from least-dependent to most-dependent (Config before Database)
  • Providers can read previously registered dependencies via context.read<T>()
  • Use nullable types (User?) for optional dependencies like authenticated user
  • Keep the global middleware as the single source of truth for DI wiring
  • Prefer constructor injection in services (accept interfaces, not concrete classes)

Error Handling

Exception Hierarchy

class AppException implements Exception {
  final String message;
  const AppException(this.message);
}

class ValidationException extends AppException {
  final Map<String, List<String>> errors;
  const ValidationException(super.message, [this.errors = const {}]);
}

class NotFoundException extends AppException {
  const NotFoundException(super.message);
}

class UnauthorizedException extends AppException {
  const UnauthorizedException([super.message = 'Unauthorized']);
}

class ForbiddenException extends AppException {
  const ForbiddenException([super.message = 'Forbidden']);
}

Global Error Handler Middleware

Middleware errorHandler() {
  return (handler) {
    return (context) async {
      try {
        return await handler(context);
      } on ValidationException catch (e) {
        return Response.json(
          {'error': e.message, 'errors': e.errors},
          statusCode: HttpStatus.unprocessableEntity,
        );
      } on NotFoundException catch (e) {
        return Response.json(
          {'error': e.message}, statusCode: HttpStatus.notFound);
      } on UnauthorizedException catch (e) {
        return Response.json(
          {'error': e.message}, statusCode: HttpStatus.unauthorized);
      } on ForbiddenException catch (e) {
        return Response.json(
          {'error': e.message}, statusCode: HttpStatus.forbidden);
      } catch (e, stack) {
        print('Unhandled: $e\n$stack');
        return Response.json(
          {'error': 'Internal server error'},
          statusCode: HttpStatus.internalServerError,
        );
      }
    };
  };
}

Models

Use freezed + json_serializable for immutable, serializable models:


class User with _$User {
  const User._();

  const factory User({
    required String id,
    required String email,
    required String name,
    (false) bool isAdmin,
    (includeToJson: false) String? passwordHash,
    required DateTime createdAt,
    DateTime? updatedAt,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

Run code generation after model changes:

dart run build_runner build --delete-conflicting-outputs

Commands

# Scaffold a new project
dart_frog create myapp

# Development server with hot reload
dart_frog dev

# Production build
dart_frog build

# Run the compiled server
./build/bin/server

# Generate a new route file
dart_frog new route /api/v1/users

# Generate a new middleware file
dart_frog new middleware auth

# Run tests
dart test

# Code generation (freezed, json_serializable)
dart run build_runner build --delete-conflicting-outputs

# Format and analyze
dart format .
dart analyze

Testing

Route Handler Tests

Use mocktail to mock RequestContext and injected services:

class MockRequestContext extends Mock implements RequestContext {}
class MockUserService extends Mock implements UserService {}

void main() {
  late MockRequestContext context;
  late MockUserService userService;

  setUp(() {
    context = MockRequestContext();
    userService = MockUserService();
    when(() => context.read<UserService>()).thenReturn(userService);
  });

  test('GET returns users list', () async {
    when(() => userService.getAll(page: any(named: 'page'),
        limit: any(named: 'limit')))
        .thenAnswer((_) async => [mockUser]);

    when(() => context.request).thenReturn(
      Request.get(Uri.parse('http://localhost/api/v1/users')),
    );

    final response = await route.onRequest(context);
    expect(response.statusCode, equals(HttpStatus.ok));
  });
}

Testing Rules

  • Mirror route paths in test file paths: routes/api/v1/users/index.dart -> test/routes/api/v1/users/index_test.dart
  • Mock all injected dependencies via context.read<T>()
  • Test each HTTP method separately
  • Test error cases (validation, not found, unauthorized)
  • Coverage target: >80% for route handlers and services

Advanced Topics

For detailed patterns and examples, see:

External References

Weekly Installs
5
Repository
ar4mirez/samuel
GitHub Stars
3
First Seen
14 days ago
Installed on
opencode5
gemini-cli5
codebuddy5
github-copilot5
codex5
kimi-cli5