Dart Frog — Backend in Pure Dart

Deep Dive · Backend Development

Dart Frog — Backend in Pure Dart

Everything you need to know about building production-ready REST APIs, middleware, and full-stack Dart apps with Dart Frog.

🐸 Dart Frog v2.x  ·  30 min read  ·  Beginner → Advanced
Section 01

What is Dart Frog?

Open Source File-based Routing Built on Shelf Very Good Ventures

Dart Frog is an opinionated, fast backend framework for Dart, built on top of the official shelf package. It was open-sourced by Very Good Ventures and takes heavy inspiration from Next.js and Remix — bringing file-based routing and a clean developer experience to the Dart ecosystem.

The core idea is simple: your file structure is your API structure. Drop a file in routes/users.dart and you have a /users endpoint. No router registration, no controller boilerplate.

💡
Dart Frog's real value comes when you're already building a Flutter app. You can share model classes, DTOs, and utility logic between your mobile frontend and Dart backend — a genuine full-stack Dart story.

A Quick History

  • 2022 — Initial ReleaseVery Good Ventures releases Dart Frog v0.1 as an experiment for server-side Dart.
  • 2022 Q4 — Rapid GrowthFile-based routing, hot reload, and middleware support gain traction in the Flutter community.
  • 2023 — v1.x StableProduction-level stability, WebSocket support, improved DI, and Docker support land.
  • 2024 — v2.x + Dart 3Full Dart 3 support, records/patterns integration, improved CLI tooling, and cloud-run optimizations.

Section 02

Why Use Dart Frog?

There are dozens of backend frameworks out there. Here's where Dart Frog earns its keep — and where it honestly doesn't.

✅ Strong Reasons To Use It

  • Shared Dart models with Flutter frontend
  • Same language across entire stack
  • Fast cold starts (AOT compilation)
  • Strong typing catches bugs early
  • Hot reload during development
  • Small binary, great for containers
  • File-based routing, zero boilerplate
  • Built-in DI via request context

⚠️ When To Avoid It

  • Heavy ORM / complex migrations needed
  • Kafka, RabbitMQ integrations required
  • Team has zero Dart experience
  • Lots of third-party SDK integrations
  • Large team (hiring risk)
  • Need mature APM tooling (Datadog etc.)
  • Project with massive traffic scaling needs
🎯
Best use case: A Flutter app's Backend for Frontend (BFF). A thin Dart Frog layer handles your app's API shape, auth tokens, and data transformation — while heavy business logic stays in your existing backend. You get shared models without betting everything on a young ecosystem.

Section 03

Installation & Project Setup

Dart Frog has a CLI that handles scaffolding, hot-reload, and build. Let's get a project up and running in under 2 minutes.

Prerequisites

  • Dart SDK 3.0 or later — check with dart --version
  • Dart Frog CLI installed globally
  • Docker (optional, for containerized deployment)

Install the CLI

bash# Activate globally via pub dart pub global activate dart_frog_cli # Verify installation dart_frog --version

Create a New Project

bash# Scaffold a new project dart_frog create my_api # Navigate into project cd my_api # Start dev server with hot reload dart_frog dev

The dev server starts at http://localhost:8080 with hot reload enabled. Any change to your routes/ directory reflects instantly — no restart needed.

Generated Project Structure

my_api/ ├── routes/ // your API endpoints live here │ ├── index.dart // handles GET / │ └── users/ │ ├── index.dart // handles GET/POST /users │ └── [id].dart // handles /users/:id (dynamic) ├── middleware/ // request interceptors │ └── _middleware.dart ├── pubspec.yaml └── dart_frog.yaml // framework config
⚠️
Files prefixed with _ (like _middleware.dart) are not routes. They are middleware files scoped to their directory. This convention is important to understand early.

Section 04

File-Based Routing (Deep Dive)

Your directory structure inside routes/ becomes your URL structure — no manual router config needed.

Routing Rules

File PathHTTP RouteNotes
routes/index.dartGET /Root handler
routes/users/index.dartGET/POST /usersCollection endpoint
routes/users/[id].dart/users/:idDynamic segment
routes/users/[id]/posts.dart/users/:id/postsNested dynamic route
routes/[...slug].dart/any/path/hereCatch-all route
routes/_middleware.dartApplies to all routes in this dir

Dynamic Route Example

dart// routes/users/[id].dart import 'package:dart_frog/dart_frog.dart'; Response onRequest(RequestContext context, String id) { return switch (context.request.method) { HttpMethod.get => _getUser(context, id), HttpMethod.put => _updateUser(context, id), HttpMethod.delete => _deleteUser(context, id), _ => Response(statusCode: 405), }; } Response _getUser(RequestContext context, String id) { return Response.json(body: {'id': id, 'name': 'Nachiketa'}); }

The route parameter id is injected as a second argument to onRequest — Dart Frog handles the extraction automatically based on the filename [id].dart.

Query Parameters

dart// GET /products?page=2&limit=20 Response onRequest(RequestContext context) { final params = context.request.uri.queryParameters; final page = int.tryParse(params['page'] ?? '1') ?? 1; final limit = int.tryParse(params['limit'] ?? '10') ?? 10; return Response.json(body: { 'page': page, 'limit': limit, 'data': [], }); }

Section 05

Request Handlers In Depth

Every route file must export an onRequest function. It receives a RequestContext and must return a Response.

Reading the Request Body

dart// routes/auth/login.dart Future<Response> onRequest(RequestContext context) async { if (context.request.method != HttpMethod.post) { return Response(statusCode: 405); } final body = await context.request.json() as Map<String, dynamic>; final email = body['email'] as String?; final password = body['password'] as String?; if (email == null || password == null) { return Response.json( statusCode: 400, body: {'error': 'email and password required'}, ); } return Response.json(body: {'token': 'jwt_token_here'}); }

Response Helpers

📦
Response.json()
Returns a JSON response. Automatically sets Content-Type: application/json.
📝
Response()
Plain text or custom body with full control over status code and headers.
↩️
Response.redirect()
Sends a 301/302 redirect. Useful for auth flows and canonical URL handling.
🌊
Response(body: Stream)
Stream large payloads or enable Server-Sent Events with a byte stream body.

Section 06

Middleware — The Interceptor Layer

Middleware runs before and after every request in its scope. Perfect for auth, logging, CORS, rate limiting, and injecting services.

Middleware Scoping

routes/ ├── _middleware.dart // applies to ALL routes ├── index.dart └── admin/ ├── _middleware.dart // applies only to /admin/* routes └── dashboard.dart

CORS Middleware

dart// routes/_middleware.dart Handler middleware(Handler handler) { return (RequestContext context) async { if (context.request.method == HttpMethod.options) { return Response(statusCode: 204, headers: _corsHeaders()); } final response = await handler(context); return response.copyWith( headers: {...response.headers, ..._corsHeaders()}, ); }; } Map<String, String> _corsHeaders() => { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', };

JWT Auth Middleware

dart// routes/api/_middleware.dart Handler middleware(Handler handler) { return (RequestContext context) async { final auth = context.request.headers['Authorization']; if (auth == null || !auth.startsWith('Bearer ')) { return Response.json( statusCode: 401, body: {'error': 'Missing or invalid token'}, ); } try { final jwt = JWT.verify(auth.substring(7), SecretKey('your-secret')); return handler(context.provide<Map>(() => jwt.payload as Map)); } catch (_) { return Response.json(statusCode: 401, body: {'error': 'Invalid token'}); } }; }
🔗
Middleware is composable and stackable. A global logging middleware in routes/_middleware.dart and an auth middleware in routes/api/_middleware.dart both run in order for /api/* requests.

Section 07

Dependency Injection via RequestContext

No annotations, no DI container. Dart Frog uses context.provide<T>() to inject dependencies and context.read<T>() to consume them. Elegant, testable, and type-safe.

Provide a Service in Middleware

dart// routes/_middleware.dart Handler middleware(Handler handler) { final db = DatabaseConnection(...); final repo = PostgresUserRepository(db); return (RequestContext context) { return handler( context.provide<UserRepository>(() => repo), ); }; }

Consume in a Route Handler

dart// routes/users/index.dart Future<Response> onRequest(RequestContext context) async { final repo = context.read<UserRepository>(); final users = await repo.findAll(); return Response.json( body: users.map((u) => u.toJson()).toList(), ); }

Testing becomes trivial — inject a MockUserRepository in tests and the handler never needs to change.


Section 08

WebSockets

Dart Frog has first-class WebSocket support via the dart_frog_web_socket package.

dart// routes/ws.dart import 'package:dart_frog_web_socket/dart_frog_web_socket.dart'; Handler get onRequest { return webSocketHandler( (WebSocketChannel channel, String? protocol) { channel.stream.listen( (message) { channel.sink.add(jsonEncode({ 'echo': message, 'timestamp': DateTime.now().toIso8601String(), })); }, onDone: () => print('Client disconnected'), ); }, ); }
📡
For real-time features in Flutter fintech apps (live prices, notifications), share the event model class between Flutter and Dart Frog — no JSON schema mismatch, ever.

Section 09

Testing — The Right Way

The dart_frog_test package provides a TestClient for firing real HTTP requests against your app without a running server.

Unit Test a Route

dartimport 'package:dart_frog_test/dart_frog_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; class MockUserRepository extends Mock implements UserRepository {} void main() { test('GET /users returns 200', () async { final mockRepo = MockUserRepository(); when(() => mockRepo.findAll()).thenAnswer( (_) async => [User(id: '1', name: 'Nachiketa')], ); final context = buildRequestContext( uri: Uri.parse('/users'), method: HttpMethod.get, ).provide<UserRepository>(() => mockRepo); final response = await onRequest(context); expect(response.statusCode, equals(200)); }); }

Section 10

Deployment

Docker (Recommended)

bashdart_frog build docker build -t my_api . docker run -p 8080:8080 my_api

Cloud Run (GCP)

bashgcloud run deploy my-api \ --source . \ --platform managed \ --region asia-south1 \ --allow-unauthenticated

Native Binary (AOT)

bash# Compiles to a self-contained ~5MB binary dart compile exe build/bin/server.dart -o server ./server
🐳
Docker / Fly.io
Small ~50MB images. Best for consistent environments and fast deploys.
☁️
Cloud Run
Scale-to-zero serverless. Dart's fast cold starts are a great fit here.
Railway / Render
PaaS with Docker support and easy GitHub CI/CD integration.

Section 11

Dart Frog vs. Node / Python / Go

FeatureDart FrogExpress (Node)FastAPI (Python)Go (Gin)
File-based routing✓ Native
Type safetyStrongOptional (TS)OptionalStrong
Shared models w/ mobileFlutter ✓SeparateSeparateSeparate
Ecosystem maturityYoungVery matureMatureMature
ORM supportLimitedRich (Prisma)SQLAlchemyGORM
Cold start (serverless)Very fast (AOT)MediumMediumVery fast
Docker image size~50MB~200MB+~150MB+~20MB
Community sizeSmallHugeLargeLarge

Section 12

Limitations & Honest Gaps

Being honest is more useful than being a fanboy. Here's what to know before committing to Dart Frog in production.

🗄️
No Production ORM
No Prisma / TypeORM equivalent. The postgres package works but you're writing raw SQL or building your own repository layer.
📦
Thin Package Ecosystem
pub.dev backend packages are scarce. Kafka, Redis, advanced auth libraries all need workarounds.
👥
Hiring Difficulty
Dart backend devs are rare. If your team grows or turns over, this is a serious staffing risk.
📊
No APM Integration
Datadog, New Relic have minimal Dart support. Observability requires manual instrumentation.
📚
Limited Community Docs
Fewer tutorials and Stack Overflow answers compared to established frameworks.
🔌
Third-party SDK Gap
Stripe, Twilio, SendGrid, Firebase Admin — official Dart SDKs are thin or REST-only.

The Golden Use Case 🎯

Dart Frog shines as a BFF (Backend for Frontend) layer for a Flutter app. Use it as a thin API gateway sharing your Dart models, handling auth, and shaping data — while heavier business logic stays in a mature backend. That is the right scope for Dart Frog right now.

Written for the Dart & Flutter Community

Dart Frog is built by Very Good Ventures · Official docs: dartfrog.vgv.dev · Source on GitHub

Post a Comment

0 Comments