dart-frog

Dart Frog Framework Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dart-frog" with this command: npx skills add ar4mirez/samuel/ar4mirez-samuel-dart-frog

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:

@freezed class User with $User { const User.();

const factory User({ required String id, required String email, required String name, @Default(false) bool isAdmin, @JsonKey(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:

  • references/patterns.md -- Route patterns, WebSocket, database integration, authentication, deployment

External References

  • Dart Frog Documentation

  • Dart Frog GitHub

  • Very Good Ventures Blog

  • Dart Server Tutorial

  • Shelf Package

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

actix-web

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

blazor

No summary provided by upstream source.

Repository SourceNeeds Review
General

fiber

No summary provided by upstream source.

Repository SourceNeeds Review