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.
What is Dart Frog?
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.
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.
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
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
# Activate globally via pub
dart pub global activate dart_frog_cli
# Verify installation
dart_frog --versionCreate a New Project
# Scaffold a new project
dart_frog create my_api
# Navigate into project
cd my_api
# Start dev server with hot reload
dart_frog devThe 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
_ (like _middleware.dart) are not routes. They are middleware files scoped to their directory. This convention is important to understand early.File-Based Routing (Deep Dive)
Your directory structure inside routes/ becomes your URL structure — no manual router config needed.
Routing Rules
| File Path | HTTP Route | Notes |
|---|---|---|
routes/index.dart | GET / | Root handler |
routes/users/index.dart | GET/POST /users | Collection endpoint |
routes/users/[id].dart | /users/:id | Dynamic segment |
routes/users/[id]/posts.dart | /users/:id/posts | Nested dynamic route |
routes/[...slug].dart | /any/path/here | Catch-all route |
routes/_middleware.dart | — | Applies to all routes in this dir |
Dynamic Route Example
// 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
// 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': [],
});
}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
// 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
Content-Type: application/json.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
CORS Middleware
// 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
// 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'});
}
};
}routes/_middleware.dart and an auth middleware in routes/api/_middleware.dart both run in order for /api/* requests.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
// 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
// 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.
WebSockets
Dart Frog has first-class WebSocket support via the dart_frog_web_socket package.
// 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'),
);
},
);
}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
import '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));
});
}Deployment
Docker (Recommended)
dart_frog build
docker build -t my_api .
docker run -p 8080:8080 my_apiCloud Run (GCP)
gcloud run deploy my-api \
--source . \
--platform managed \
--region asia-south1 \
--allow-unauthenticatedNative Binary (AOT)
# Compiles to a self-contained ~5MB binary
dart compile exe build/bin/server.dart -o server
./serverDart Frog vs. Node / Python / Go
| Feature | Dart Frog | Express (Node) | FastAPI (Python) | Go (Gin) |
|---|---|---|---|---|
| File-based routing | ✓ Native | ✗ | ✗ | ✗ |
| Type safety | Strong | Optional (TS) | Optional | Strong |
| Shared models w/ mobile | Flutter ✓ | Separate | Separate | Separate |
| Ecosystem maturity | Young | Very mature | Mature | Mature |
| ORM support | Limited | Rich (Prisma) | SQLAlchemy | GORM |
| Cold start (serverless) | Very fast (AOT) | Medium | Medium | Very fast |
| Docker image size | ~50MB | ~200MB+ | ~150MB+ | ~20MB |
| Community size | Small | Huge | Large | Large |
Limitations & Honest Gaps
Being honest is more useful than being a fanboy. Here's what to know before committing to Dart Frog in production.
postgres package works but you're writing raw SQL or building your own repository layer.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
0 Comments