<FlutterSolution/> flutter · dart · clean architecture

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.dart—Applies 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

Nachiketa Pandey
// written by

Nachiketa Pandey

Senior Flutter Developer · Cross-Platform & Fintech Applications

Building production Flutter apps at Kotak Neo — clean architecture, BLoC, agentic AI integrations, and fintech-grade security. Sharing everything I learn, one post at a time.

Flutter & Dart Clean Architecture BLoC Firebase FinTech
View Full Portfolio

Comments

↑
Nachiketa
Meet Author// tap to open
▶