<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ dart_frog - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ dart_frog - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 12 Jun 2026 05:19:29 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/dartfrog/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ From Flutter to Backend: How to Build Production-Grade REST APIs with Dart and Dart Frog ]]>
                </title>
                <description>
                    <![CDATA[ Dart backend frameworks exist on a spectrum. At the minimal end sits Shelf, with raw primitives and full control. You wire everything yourself. At the maximal end sits Serverpod. It's a full framework ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-production-grade-rest-apis-with-dart-and-dart-frog/</link>
                <guid isPermaLink="false">6a2b553bb84c3c44ce471560</guid>
                
                    <category>
                        <![CDATA[ dart_frog ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mobile Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Fri, 12 Jun 2026 00:39:23 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a80b24db-c53e-4d36-85cd-0cb999676145.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Dart backend frameworks exist on a spectrum. At the <a href="https://www.freecodecamp.org/news/how-to-build-and-ship-production-rest-apis-with-dart-and-shelf/">minimal end sits Shelf,</a> with raw primitives and full control. You wire everything yourself. <a href="https://www.freecodecamp.org/news/how-to-build-production-grade-rest-apis-with-dart-and-serverpod/">At the maximal end sits Serverpod</a>. It's a full framework with code generation and opinionated conventions. The framework makes most structural decisions for you.</p>
<p>Dart Frog lives in the middle, and for many Flutter engineers, it's the most natural fit.</p>
<p>Dart Frog is a fast, minimalistic backend framework built on top of Shelf, originally created by Very Good Ventures and now maintained independently. It takes the file-based routing model popularized by Next.js and Remix, applies it to Dart, and wraps it with a clean CLI that handles development server, hot reload, production builds, and Docker generation, all out of the box.</p>
<p>You write a Dart file in the routes/ directory, export an onRequest function, and Dart Frog handles the routing automatically. No router configuration, no handler registration, no mounting. The file system is the router.</p>
<p>In this article, we'll build a User and Profile Management REST API (the same one we built in the linked articles above) using Dart Frog, connect it to PostgreSQL, add JWT authentication, and deploy it to Fly.io.</p>
<p>By the end you'll understand Dart Frog's routing model deeply, and you'll have a clear picture of where it fits compared to Shelf and Serverpod.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-dart-frog-differs-from-shelf-and-serverpod">How Dart Frog Differs from Shelf and Serverpod</a></p>
</li>
<li><p><a href="#heading-installing-dart-frog">Installing Dart Frog</a></p>
</li>
<li><p><a href="#heading-creating-the-project">Creating the Project</a></p>
</li>
<li><p><a href="#heading-understanding-the-project-structure">Understanding the Project Structure</a></p>
</li>
<li><p><a href="#heading-dart-frog-core-concepts">Dart Frog Core Concepts</a></p>
<ul>
<li><p><a href="#heading-file-based-routing">File-Based Routing</a></p>
</li>
<li><p><a href="#heading-the-requestcontext">The RequestContext</a></p>
</li>
<li><p><a href="#heading-middleware-and-dependency-injection">Middleware and Dependency Injection</a></p>
</li>
<li><p><a href="#heading-dynamic-routes">Dynamic Routes</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-setting-up-the-database">Setting Up the Database</a></p>
<ul>
<li><p><a href="#heading-docker-compose-for-postgresql">Docker Compose for PostgreSQL</a></p>
</li>
<li><p><a href="#heading-environment-configuration">Environment Configuration</a></p>
</li>
<li><p><a href="#heading-database-connection-manager">Database Connection Manager</a></p>
</li>
<li><p><a href="#heading-migrations">Migrations</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-defining-the-models">Defining the Models</a></p>
</li>
<li><p><a href="#heading-building-the-repositories">Building the Repositories</a></p>
<ul>
<li><p><a href="#heading-user-repository">User Repository</a></p>
</li>
<li><p><a href="#heading-profile-repository">Profile Repository</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-authentication-service">Authentication Service</a></p>
</li>
<li><p><a href="#heading-middleware">Middleware</a></p>
<ul>
<li><p><a href="#heading-database-middleware">Database Middleware</a></p>
</li>
<li><p><a href="#heading-auth-middleware">Auth Middleware</a></p>
</li>
<li><p><a href="#heading-error-middleware">Error Middleware</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-building-the-routes">Building the Routes</a></p>
<ul>
<li><p><a href="#heading-auth-routes">Auth Routes</a></p>
</li>
<li><p><a href="#heading-user-routes">User Routes</a></p>
</li>
<li><p><a href="#heading-profile-routes">Profile Routes</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-wiring-the-middleware-pipeline">Wiring the Middleware Pipeline</a></p>
</li>
<li><p><a href="#heading-testing-the-api">Testing the API</a></p>
</li>
<li><p><a href="#heading-deployment">Deployment</a></p>
<ul>
<li><p><a href="#heading-production-build">Production Build</a></p>
</li>
<li><p><a href="#heading-deploying-to-flyio">Deploying to Fly.io</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, you should have:</p>
<ul>
<li><p>Comfortable familiarity with Dart and Flutter development</p>
</li>
<li><p>Understanding of REST API concepts, endpoints, HTTP methods, status codes</p>
</li>
<li><p>Docker Desktop installed and running</p>
</li>
<li><p>A Fly.io account for deployment</p>
</li>
</ul>
<h2 id="heading-how-dart-frog-differs-from-shelf-and-serverpod">How Dart Frog Differs from Shelf and Serverpod</h2>
<p>Understanding where Dart Frog sits in relation to the other two frameworks helps you make the right choice for each project.</p>
<p>Shelf gives you a Router and you mount handlers manually. Your folder structure has nothing to do with your URL structure. You decide what goes where.</p>
<p>Serverpod generates your routes from endpoint class names and method names. You define a class, run a generator, and the URL is derived automatically.</p>
<p>Dart Frog maps your file system directly to your URL structure. A file at routes/users/index.dart becomes the /users endpoint. A file at routes/users/[id].dart becomes /users/:id. No configuration, no registration, no generation step. The file is the route.</p>
<p>This model will feel immediately intuitive to Flutter engineers who have worked with Next.js or any modern web framework. It's also significantly easier to navigate in a team. You look at the folder structure and you instantly know what endpoints exist.</p>
<p>The other key difference is the RequestContext. Where Shelf passes a raw Request to handlers, Dart Frog wraps it in a RequestContext that carries both the request and any values injected by middleware. This is Dart Frog's dependency injection mechanism, and it's elegant.</p>
<h2 id="heading-installing-dart-frog">Installing Dart Frog</h2>
<p>Install the Dart Frog CLI:</p>
<pre><code class="language-bash">dart pub global activate dart_frog_cli
</code></pre>
<p>Verify the installation:</p>
<pre><code class="language-bash">dart_frog --version
</code></pre>
<h2 id="heading-creating-the-project">Creating the Project</h2>
<pre><code class="language-bash">dart_frog create user_profile_api
cd user_profile_api
</code></pre>
<p>Start the development server with hot reload:</p>
<pre><code class="language-bash">dart_frog dev
</code></pre>
<p>Visit <a href="http://localhost:8080">http://localhost:8080</a> and you'll see the default welcome response. The dev server watches for file changes and reloads automatically. No restart needed as you build.</p>
<h2 id="heading-understanding-the-project-structure">Understanding the Project Structure</h2>
<pre><code class="language-plaintext">user_profile_api/
  routes/
    index.dart              ← GET /
  pubspec.yaml
  analysis_options.yaml
</code></pre>
<p>That's the entire starting structure. Clean and minimal. Everything we add will extend from here.</p>
<p>After building our API, the full structure will look like this:</p>
<pre><code class="language-plaintext">user_profile_api/
  routes/
    _middleware.dart         ← global middleware pipeline
    index.dart               ← GET /
    auth/
      login.dart             ← POST /auth/login
      register.dart          ← POST /auth/register
    users/
      index.dart             ← GET /users
      [id].dart              ← GET, PUT, DELETE /users/:id
      [id]/
        profile.dart         ← GET, POST, PUT /users/:id/profile
  lib/
    config/
      database.dart
      env.dart
    models/
      user.dart
      profile.dart
    repositories/
      user_repository.dart
      profile_repository.dart
    services/
      auth_service.dart
    middleware/
      auth_middleware.dart
      error_middleware.dart
  pubspec.yaml
</code></pre>
<p>The routes/ folder is the heart of a Dart Frog project. The lib/ folder holds all shared logic that routes import. This separation is clean and deliberate: routing concerns live in routes/, while business logic lives in lib/.</p>
<h2 id="heading-dart-frog-core-concepts">Dart Frog Core Concepts</h2>
<h3 id="heading-file-based-routing">File-Based Routing</h3>
<p>Every .dart file in the routes/ directory is a route. The file path determines the URL path:</p>
<table>
<thead>
<tr>
<th>File</th>
<th>URL</th>
</tr>
</thead>
<tbody><tr>
<td>routes/index.dart</td>
<td>/</td>
</tr>
<tr>
<td>routes/users/index.dart</td>
<td>/users</td>
</tr>
<tr>
<td>routes/users/[id].dart</td>
<td>/users/:id</td>
</tr>
<tr>
<td>routes/auth/login.dart</td>
<td>/auth/login</td>
</tr>
<tr>
<td>routes/users/[id]/profile.dart</td>
<td>/users/:id/profile</td>
</tr>
</tbody></table>
<p>Every route file must export an onRequest function:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';

Future&lt;Response&gt; onRequest(RequestContext context) async {
  return Response.json(body: {'message': 'Hello from Dart Frog'});
}
</code></pre>
<p>That's the entire contract. One function, one file, one route. Dart Frog generates the internal routing glue automatically when you run dart_frog dev or dart_frog build.</p>
<h3 id="heading-the-requestcontext">The RequestContext</h3>
<p>RequestContext is the object passed to every route handler and middleware. It's more than just the HTTP request: it's a container for the request and any values that middleware has injected:</p>
<pre><code class="language-dart">Future&lt;Response&gt; onRequest(RequestContext context) async {
  // The raw HTTP request
  final request = context.request;

  // HTTP method
  print(request.method); // GET, POST, etc.

  // Path parameters (for dynamic routes like [id].dart)
  final id = context.request.uri.pathSegments.last;

  // Query parameters
  final page = request.uri.queryParameters['page'];

  // Request body
  final body = await request.json() as Map&lt;String, dynamic&gt;;

  // Values injected by middleware
  final db = context.read&lt;DatabaseConnection&gt;();
  final currentUser = context.read&lt;AuthenticatedUser&gt;();

  return Response.json(body: {'ok': true});
}
</code></pre>
<p>context.read() is the dependency injection mechanism. Middleware provides values, and routes consume them. This keeps routes clean and testable: a route handler doesn't know how a database connection was created, it just reads it from context.</p>
<h3 id="heading-middleware-and-dependency-injection">Middleware and Dependency Injection</h3>
<p>A <code>_middleware.dart</code> file in any route folder applies middleware to all routes in that folder and its subfolders. A <code>_middleware.dart</code> at the root routes/ level applies globally.</p>
<p>Middleware in Dart Frog uses the provider pattern to inject values into the context:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';

Handler middleware(Handler handler) {
  return handler.use(
    provider&lt;DatabaseConnection&gt;(
      (context) =&gt; DatabaseConnection.instance,
    ),
  );
}
</code></pre>
<p>Any route in the same folder, or any subfolder, can then call context.read() to get the connection. No global singletons, no manual passing. The context carries it.</p>
<p>Middleware functions can also intercept requests before they reach the route handler, making them perfect for authentication:</p>
<pre><code class="language-dart">Handler middleware(Handler handler) {
  return (context) async {
    final authHeader = context.request.headers['authorization'];

    if (authHeader == null) {
      return Response.json(
        statusCode: 401,
        body: {'error': 'Authorization required'},
      );
    }

    // Verify token and inject user
    final user = verifyToken(authHeader);
    return handler(context.provide&lt;AuthenticatedUser&gt;(() =&gt; user));
  };
}
</code></pre>
<h3 id="heading-dynamic-routes">Dynamic Routes</h3>
<p>A file named [id].dart matches any single path segment. Inside the handler, extract the parameter from the URL:</p>
<pre><code class="language-dart">Future&lt;Response&gt; onRequest(RequestContext context, String id) async {
  // id is automatically passed as a parameter for dynamic routes
  return Response.json(body: {'userId': id});
}
</code></pre>
<p>Dart Frog passes dynamic route parameters as additional arguments to onRequest. This is cleaner than parsing them manually from the URL.</p>
<h2 id="heading-setting-up-the-database">Setting Up the Database</h2>
<h3 id="heading-docker-compose-for-postgresql">Docker Compose for PostgreSQL</h3>
<p>Create docker-compose.yml in the project root:</p>
<pre><code class="language-yaml">version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    container_name: user_profile_db
    environment:
      POSTGRES_DB: user_profile_api
      POSTGRES_USER: dart_user
      POSTGRES_PASSWORD: dart_password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dart_user -d user_profile_api"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:
</code></pre>
<p>Start the database:</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<h3 id="heading-environment-configuration">Environment Configuration</h3>
<p>Add dependencies to pubspec.yaml:</p>
<pre><code class="language-yaml">dependencies:
  dart_frog: ^1.4.0
  dart_frog_auth: ^0.1.0
  postgres: ^3.3.0
  dart_jsonwebtoken: ^2.12.0
  bcrypt: ^1.1.3
  dotenv: ^4.1.0

dev_dependencies:
  dart_frog_cli: ^1.2.0
  test: ^1.24.0
  dart_frog_test: ^0.1.0
</code></pre>
<p>Run dart pub get.</p>
<p>Create .env:</p>
<pre><code class="language-plaintext">DB_HOST=localhost
DB_PORT=5432
DB_NAME=user_profile_api
DB_USER=dart_user
DB_PASSWORD=dart_password
JWT_SECRET=your_super_secret_key_change_this_in_production
JWT_EXPIRY_HOURS=24
PORT=8080
</code></pre>
<p>Create lib/config/env.dart:</p>
<pre><code class="language-dart">import 'package:dotenv/dotenv.dart';

class Env {
  static late final DotEnv _env;

  static void load() {
    _env = DotEnv(includePlatformEnvironment: true)..load();
  }

  static String get dbHost =&gt; _env['DB_HOST'] ?? 'localhost';
  static int get dbPort =&gt; int.parse(_env['DB_PORT'] ?? '5432');
  static String get dbName =&gt; _env['DB_NAME'] ?? 'user_profile_api';
  static String get dbUser =&gt; _env['DB_USER'] ?? 'dart_user';
  static String get dbPassword =&gt; _env['DB_PASSWORD'] ?? '';
  static String get jwtSecret =&gt; _env['JWT_SECRET'] ?? '';
  static int get jwtExpiryHours =&gt;
      int.parse(_env['JWT_EXPIRY_HOURS'] ?? '24');
}
</code></pre>
<h3 id="heading-database-connection-manager">Database Connection Manager</h3>
<p>Create lib/config/database.dart:</p>
<pre><code class="language-dart">import 'package:postgres/postgres.dart';
import 'env.dart';

class Database {
  static Connection? _connection;

  static Future&lt;Connection&gt; get connection async {
    if (_connection != null) return _connection!;
    _connection = await Connection.open(
      Endpoint(
        host: Env.dbHost,
        port: Env.dbPort,
        database: Env.dbName,
        username: Env.dbUser,
        password: Env.dbPassword,
      ),
      settings: const ConnectionSettings(sslMode: SslMode.disable),
    );
    print('Database connected');
    return _connection!;
  }

  static Future&lt;void&gt; runMigrations() async {
    final conn = await connection;
    await conn.execute('''
      CREATE TABLE IF NOT EXISTS users (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        email VARCHAR(255) UNIQUE NOT NULL,
        password_hash VARCHAR(255) NOT NULL,
        first_name VARCHAR(100) NOT NULL,
        last_name VARCHAR(100) NOT NULL,
        is_active BOOLEAN DEFAULT TRUE,
        created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
      );

      CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

      CREATE TABLE IF NOT EXISTS profiles (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        bio TEXT,
        avatar_url VARCHAR(500),
        phone VARCHAR(20),
        location VARCHAR(255),
        website VARCHAR(500),
        created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
        UNIQUE(user_id)
      );

      CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
    ''');
    print('Migrations applied');
  }
}
</code></pre>
<h3 id="heading-migrations">Migrations</h3>
<p>Dart Frog projects have a main.dart entry point generated during dart_frog build. For the development server, migrations are best run from the project entrypoint. Create main.dart in the project root:</p>
<pre><code class="language-dart">import 'dart:io';
import 'package:dart_frog/dart_frog.dart';
import 'lib/config/database.dart';
import 'lib/config/env.dart';

Future&lt;HttpServer&gt; run(Handler handler, InternetAddress ip, int port) async {
  Env.load();
  await Database.runMigrations();
  return serve(handler, ip, port);
}
</code></pre>
<p>This run function is Dart Frog's server lifecycle hook. It runs before the server starts accepting requests, giving us the right place to load environment variables and run migrations.</p>
<h2 id="heading-defining-the-models">Defining the Models</h2>
<p>With the database layer in place, we need Dart classes to represent the data coming in and out of it.</p>
<p>The User model maps to the users table and handles conversion between database rows and Dart objects. The Profile model does the same for the profiles table. Both models follow the same pattern: a factory constructor for reading from the database and a <code>toJson</code> method for sending data back to the client.</p>
<p>Note that <code>toJson</code> on the User model deliberately excludes the password hash. You should never return credential data in an API response.</p>
<p>Create lib/models/user.dart:</p>
<pre><code class="language-dart">class User {
  const User({
    required this.id,
    required this.email,
    required this.passwordHash,
    required this.firstName,
    required this.lastName,
    required this.isActive,
    required this.createdAt,
    required this.updatedAt,
  });

  final String id;
  final String email;
  final String passwordHash;
  final String firstName;
  final String lastName;
  final bool isActive;
  final DateTime createdAt;
  final DateTime updatedAt;

  factory User.fromRow(Map&lt;String, dynamic&gt; row) =&gt; User(
        id: row['id'] as String,
        email: row['email'] as String,
        passwordHash: row['password_hash'] as String,
        firstName: row['first_name'] as String,
        lastName: row['last_name'] as String,
        isActive: row['is_active'] as bool,
        createdAt: row['created_at'] as DateTime,
        updatedAt: row['updated_at'] as DateTime,
      );

  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'id': id,
        'email': email,
        'firstName': firstName,
        'lastName': lastName,
        'isActive': isActive,
        'createdAt': createdAt.toIso8601String(),
        'updatedAt': updatedAt.toIso8601String(),
      };
}
</code></pre>
<p>Create lib/models/profile.dart:</p>
<pre><code class="language-dart">class Profile {
  const Profile({
    required this.id,
    required this.userId,
    this.bio,
    this.avatarUrl,
    this.phone,
    this.location,
    this.website,
    required this.createdAt,
    required this.updatedAt,
  });

  final String id;
  final String userId;
  final String? bio;
  final String? avatarUrl;
  final String? phone;
  final String? location;
  final String? website;
  final DateTime createdAt;
  final DateTime updatedAt;

  factory Profile.fromRow(Map&lt;String, dynamic&gt; row) =&gt; Profile(
        id: row['id'] as String,
        userId: row['user_id'] as String,
        bio: row['bio'] as String?,
        avatarUrl: row['avatar_url'] as String?,
        phone: row['phone'] as String?,
        location: row['location'] as String?,
        website: row['website'] as String?,
        createdAt: row['created_at'] as DateTime,
        updatedAt: row['updated_at'] as DateTime,
      );

  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'id': id,
        'userId': userId,
        'bio': bio,
        'avatarUrl': avatarUrl,
        'phone': phone,
        'location': location,
        'website': website,
        'createdAt': createdAt.toIso8601String(),
        'updatedAt': updatedAt.toIso8601String(),
      };
}
</code></pre>
<h2 id="heading-building-the-repositories">Building the Repositories</h2>
<p>Repositories are the single point of contact between the application and the database. Rather than writing SQL directly inside route handlers, we'll centralise all database operations here. This keeps the handlers clean and makes the data access logic easy to find, maintain, and test independently.</p>
<p>The UserRepository handles every operation on the users table. The ProfileRepository does the same for profiles, using userId as its primary lookup key since profiles are always accessed in the context of a specific user.</p>
<h3 id="heading-user-repository">User Repository</h3>
<p>Create lib/repositories/user_repository.dart:</p>
<pre><code class="language-dart">import 'package:postgres/postgres.dart';
import '../config/database.dart';
import '../models/user.dart';

class UserRepository {
  Future&lt;Connection&gt; get _conn =&gt; Database.connection;

  Future&lt;List&lt;User&gt;&gt; findAll() async {
    final conn = await _conn;
    final results = await conn.execute(
      'SELECT * FROM users WHERE is_active = TRUE ORDER BY created_at DESC',
    );
    return results.map((r) =&gt; User.fromRow(r.toColumnMap())).toList();
  }

  Future&lt;User?&gt; findById(String id) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('SELECT * FROM users WHERE id = @id AND is_active = TRUE'),
      parameters: {'id': id},
    );
    if (results.isEmpty) return null;
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;User?&gt; findByEmail(String email) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('SELECT * FROM users WHERE email = @email'),
      parameters: {'email': email},
    );
    if (results.isEmpty) return null;
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;User&gt; create({
    required String email,
    required String passwordHash,
    required String firstName,
    required String lastName,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        INSERT INTO users (email, password_hash, first_name, last_name)
        VALUES (@email, @passwordHash, @firstName, @lastName)
        RETURNING *
      '''),
      parameters: {
        'email': email,
        'passwordHash': passwordHash,
        'firstName': firstName,
        'lastName': lastName,
      },
    );
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;User?&gt; update({
    required String id,
    String? firstName,
    String? lastName,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        UPDATE users
        SET
          first_name = COALESCE(@firstName, first_name),
          last_name  = COALESCE(@lastName, last_name),
          updated_at = NOW()
        WHERE id = @id AND is_active = TRUE
        RETURNING *
      '''),
      parameters: {'id': id, 'firstName': firstName, 'lastName': lastName},
    );
    if (results.isEmpty) return null;
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;bool&gt; delete(String id) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        UPDATE users SET is_active = FALSE, updated_at = NOW()
        WHERE id = @id AND is_active = TRUE
        RETURNING id
      '''),
      parameters: {'id': id},
    );
    return results.isNotEmpty;
  }
}
</code></pre>
<h3 id="heading-profile-repository">Profile Repository</h3>
<p>Create lib/repositories/profile_repository.dart:</p>
<pre><code class="language-dart">import 'package:postgres/postgres.dart';
import '../config/database.dart';
import '../models/profile.dart';

class ProfileRepository {
  Future&lt;Connection&gt; get _conn =&gt; Database.connection;

  Future&lt;Profile?&gt; findByUserId(String userId) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('SELECT * FROM profiles WHERE user_id = @userId'),
      parameters: {'userId': userId},
    );
    if (results.isEmpty) return null;
    return Profile.fromRow(results.first.toColumnMap());
  }

  Future&lt;Profile&gt; create({
    required String userId,
    String? bio,
    String? avatarUrl,
    String? phone,
    String? location,
    String? website,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        INSERT INTO profiles (user_id, bio, avatar_url, phone, location, website)
        VALUES (@userId, @bio, @avatarUrl, @phone, @location, @website)
        RETURNING *
      '''),
      parameters: {
        'userId': userId,
        'bio': bio,
        'avatarUrl': avatarUrl,
        'phone': phone,
        'location': location,
        'website': website,
      },
    );
    return Profile.fromRow(results.first.toColumnMap());
  }

  Future&lt;Profile?&gt; update({
    required String userId,
    String? bio,
    String? avatarUrl,
    String? phone,
    String? location,
    String? website,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        UPDATE profiles
        SET
          bio        = COALESCE(@bio, bio),
          avatar_url = COALESCE(@avatarUrl, avatar_url),
          phone      = COALESCE(@phone, phone),
          location   = COALESCE(@location, location),
          website    = COALESCE(@website, website),
          updated_at = NOW()
        WHERE user_id = @userId
        RETURNING *
      '''),
      parameters: {
        'userId': userId,
        'bio': bio,
        'avatarUrl': avatarUrl,
        'phone': phone,
        'location': location,
        'website': website,
      },
    );
    if (results.isEmpty) return null;
    return Profile.fromRow(results.first.toColumnMap());
  }
}
</code></pre>
<h2 id="heading-authentication-service">Authentication Service</h2>
<p>Authentication in this project is handled by a dedicated AuthService that lives in lib/services/. It has one clear responsibility: the cryptographic operations that power auth: hashing passwords before storing them, verifying passwords at login, generating signed JWT tokens on success, and verifying those tokens on protected requests.</p>
<p>Keeping this logic in a service rather than spreading it across route handlers means it can be injected via middleware and consumed cleanly anywhere in the app.</p>
<p>Create lib/services/auth_service.dart:</p>
<pre><code class="language-dart">import 'package:bcrypt/bcrypt.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import '../config/env.dart';
import '../models/user.dart';

class AuthService {
  String hashPassword(String password) =&gt;
      BCrypt.hashpw(password, BCrypt.gensalt());

  bool verifyPassword(String password, String hash) =&gt;
      BCrypt.checkpw(password, hash);

  String generateToken(User user) {
    final jwt = JWT({
      'sub': user.id,
      'email': user.email,
      'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
    });
    return jwt.sign(
      SecretKey(Env.jwtSecret),
      expiresIn: Duration(hours: Env.jwtExpiryHours),
    );
  }

  JWT? verifyToken(String token) {
    try {
      return JWT.verify(token, SecretKey(Env.jwtSecret));
    } catch (_) {
      return null;
    }
  }
}
</code></pre>
<h2 id="heading-middleware">Middleware</h2>
<p>Middleware is where Dart Frog's dependency injection model does its most important work. Rather than instantiating repositories and services inside each route handler, we create them once in middleware and make them available to every handler downstream via the RequestContext.</p>
<p>This section defines three pieces of middleware: the database middleware that injects the repositories and auth service, the auth middleware that validates JWT tokens and protects routes, and the error middleware that catches unhandled exceptions and returns consistent error responses across the entire API.</p>
<h3 id="heading-database-middleware">Database Middleware</h3>
<p>Create lib/middleware/database_middleware.dart:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';
import '../repositories/user_repository.dart';
import '../repositories/profile_repository.dart';
import '../services/auth_service.dart';

Middleware databaseMiddleware() {
  return (handler) {
    return handler
        .use(provider&lt;UserRepository&gt;((_) =&gt; UserRepository()))
        .use(provider&lt;ProfileRepository&gt;((_) =&gt; ProfileRepository()))
        .use(provider&lt;AuthService&gt;((_) =&gt; AuthService()));
  };
}
</code></pre>
<p>This middleware injects the repositories and auth service into every request context. Routes read them with <code>context.read()</code> without caring how they were created.</p>
<h3 id="heading-auth-middleware">Auth Middleware</h3>
<p>Create lib/middleware/auth_middleware.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:dart_frog/dart_frog.dart';
import '../services/auth_service.dart';

Middleware authMiddleware() {
  return (handler) {
    return (context) async {
      final authHeader = context.request.headers['authorization'];

      if (authHeader == null || !authHeader.startsWith('Bearer ')) {
        return Response.json(
          statusCode: 401,
          body: {'error': 'Authorization header missing or malformed'},
        );
      }

      final token = authHeader.substring(7);
      final authService = context.read&lt;AuthService&gt;();
      final jwt = authService.verifyToken(token);

      if (jwt == null) {
        return Response.json(
          statusCode: 401,
          body: {'error': 'Invalid or expired token'},
        );
      }

      final userId = jwt.payload['sub'] as String;
      final userEmail = jwt.payload['email'] as String;

      return handler(
        context.provide&lt;Map&lt;String, String&gt;&gt;(
          () =&gt; {'userId': userId, 'userEmail': userEmail},
        ),
      );
    };
  };
}
</code></pre>
<h3 id="heading-error-middleware">Error Middleware</h3>
<p>Create lib/middleware/error_middleware.dart:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';

Middleware errorMiddleware() {
  return (handler) {
    return (context) async {
      try {
        return await handler(context);
      } on FormatException catch (e) {
        return Response.json(
          statusCode: 400,
          body: {'error': 'Invalid request body: ${e.message}'},
        );
      } catch (e, stackTrace) {
        print('Unhandled error: \(e\n\)stackTrace');
        return Response.json(
          statusCode: 500,
          body: {'error': 'An internal server error occurred'},
        );
      }
    };
  };
}
</code></pre>
<h2 id="heading-building-the-routes">Building the Routes</h2>
<p>With the models, repositories, auth service, and middleware all in place, we can now build the route handlers.</p>
<p>In Dart Frog, each file in the routes/ folder is a self-contained endpoint. Routes don't manage dependencies directly. Instead, they read what middleware has already injected into the context and call the appropriate repository or service method.</p>
<p>This section covers three groups of routes: the auth routes for registration and login, the user routes for CRUD operations, and the profile routes nested under a user's ID.</p>
<h3 id="heading-auth-routes">Auth Routes</h3>
<p>Create routes/auth/register.dart:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';
import '../../lib/repositories/user_repository.dart';
import '../../lib/services/auth_service.dart';

Future&lt;Response&gt; onRequest(RequestContext context) async {
  if (context.request.method != HttpMethod.post) {
    return Response.json(statusCode: 405, body: {'error': 'Method not allowed'});
  }

  final body = await context.request.json() as Map&lt;String, dynamic&gt;;
  final email = body['email'] as String?;
  final password = body['password'] as String?;
  final firstName = body['firstName'] as String?;
  final lastName = body['lastName'] as String?;

  if (email == null || password == null ||
      firstName == null || lastName == null) {
    return Response.json(
      statusCode: 400,
      body: {'error': 'email, password, firstName, and lastName are required'},
    );
  }

  if (password.length &lt; 8) {
    return Response.json(
      statusCode: 400,
      body: {'error': 'Password must be at least 8 characters'},
    );
  }

  final userRepo = context.read&lt;UserRepository&gt;();
  final authService = context.read&lt;AuthService&gt;();

  final existing = await userRepo.findByEmail(email);
  if (existing != null) {
    return Response.json(
      statusCode: 409,
      body: {'error': 'An account with this email already exists'},
    );
  }

  final user = await userRepo.create(
    email: email,
    passwordHash: authService.hashPassword(password),
    firstName: firstName,
    lastName: lastName,
  );

  return Response.json(
    statusCode: 201,
    body: {
      'user': user.toJson(),
      'token': authService.generateToken(user),
    },
  );
}
</code></pre>
<p>Create routes/auth/login.dart:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';
import '../../lib/repositories/user_repository.dart';
import '../../lib/services/auth_service.dart';

Future&lt;Response&gt; onRequest(RequestContext context) async {
  if (context.request.method != HttpMethod.post) {
    return Response.json(statusCode: 405, body: {'error': 'Method not allowed'});
  }

  final body = await context.request.json() as Map&lt;String, dynamic&gt;;
  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 are required'},
    );
  }

  final userRepo = context.read&lt;UserRepository&gt;();
  final authService = context.read&lt;AuthService&gt;();
  final user = await userRepo.findByEmail(email);

  if (user == null || !authService.verifyPassword(password, user.passwordHash)) {
    return Response.json(
      statusCode: 401,
      body: {'error': 'Invalid email or password'},
    );
  }

  return Response.json(
    body: {
      'user': user.toJson(),
      'token': authService.generateToken(user),
    },
  );
}
</code></pre>
<h3 id="heading-user-routes">User Routes</h3>
<p>Create routes/users/index.dart:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';
import '../../lib/repositories/user_repository.dart';

Future&lt;Response&gt; onRequest(RequestContext context) async {
  if (context.request.method != HttpMethod.get) {
    return Response.json(statusCode: 405, body: {'error': 'Method not allowed'});
  }

  final userRepo = context.read&lt;UserRepository&gt;();
  final users = await userRepo.findAll();

  return Response.json(
    body: users.map((u) =&gt; u.toJson()).toList(),
  );
}
</code></pre>
<p>Create routes/users/[id].dart:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';
import '../../lib/repositories/user_repository.dart';

Future&lt;Response&gt; onRequest(RequestContext context, String id) async {
  final userRepo = context.read&lt;UserRepository&gt;();

  switch (context.request.method) {
    case HttpMethod.get:
      return _getUser(userRepo, id);
    case HttpMethod.put:
      return _updateUser(context, userRepo, id);
    case HttpMethod.delete:
      return _deleteUser(userRepo, id);
    default:
      return Response.json(
        statusCode: 405,
        body: {'error': 'Method not allowed'},
      );
  }
}

Future&lt;Response&gt; _getUser(UserRepository repo, String id) async {
  final user = await repo.findById(id);
  if (user == null) {
    return Response.json(statusCode: 404, body: {'error': 'User not found'});
  }
  return Response.json(body: user.toJson());
}

Future&lt;Response&gt; _updateUser(
  RequestContext context,
  UserRepository repo,
  String id,
) async {
  final body = await context.request.json() as Map&lt;String, dynamic&gt;;
  final user = await repo.update(
    id: id,
    firstName: body['firstName'] as String?,
    lastName: body['lastName'] as String?,
  );
  if (user == null) {
    return Response.json(statusCode: 404, body: {'error': 'User not found'});
  }
  return Response.json(body: user.toJson());
}

Future&lt;Response&gt; _deleteUser(UserRepository repo, String id) async {
  final deleted = await repo.delete(id);
  if (!deleted) {
    return Response.json(statusCode: 404, body: {'error': 'User not found'});
  }
  return Response.json(statusCode: 204, body: null);
}
</code></pre>
<p>Notice how onRequest receives String id as a second parameter, Dart Frog automatically passes the dynamic path segment to the handler. The switch on context.request.method handles all HTTP methods in a single file which is the idiomatic Dart Frog pattern for CRUD endpoints.</p>
<h3 id="heading-profile-routes">Profile Routes</h3>
<p>Create routes/users/[id]/profile.dart:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';
import '../../../lib/repositories/user_repository.dart';
import '../../../lib/repositories/profile_repository.dart';

Future&lt;Response&gt; onRequest(RequestContext context, String id) async {
  final userRepo = context.read&lt;UserRepository&gt;();
  final profileRepo = context.read&lt;ProfileRepository&gt;();

  final user = await userRepo.findById(id);
  if (user == null) {
    return Response.json(statusCode: 404, body: {'error': 'User not found'});
  }

  switch (context.request.method) {
    case HttpMethod.get:
      return _getProfile(profileRepo, id);
    case HttpMethod.post:
      return _createProfile(context, profileRepo, id);
    case HttpMethod.put:
      return _updateProfile(context, profileRepo, id);
    default:
      return Response.json(
        statusCode: 405,
        body: {'error': 'Method not allowed'},
      );
  }
}

Future&lt;Response&gt; _getProfile(ProfileRepository repo, String userId) async {
  final profile = await repo.findByUserId(userId);
  if (profile == null) {
    return Response.json(statusCode: 404, body: {'error': 'Profile not found'});
  }
  return Response.json(body: profile.toJson());
}

Future&lt;Response&gt; _createProfile(
  RequestContext context,
  ProfileRepository repo,
  String userId,
) async {
  final existing = await repo.findByUserId(userId);
  if (existing != null) {
    return Response.json(
      statusCode: 409,
      body: {'error': 'Profile already exists for this user'},
    );
  }

  final body = await context.request.json() as Map&lt;String, dynamic&gt;;
  final profile = await repo.create(
    userId: userId,
    bio: body['bio'] as String?,
    avatarUrl: body['avatarUrl'] as String?,
    phone: body['phone'] as String?,
    location: body['location'] as String?,
    website: body['website'] as String?,
  );
  return Response.json(statusCode: 201, body: profile.toJson());
}

Future&lt;Response&gt; _updateProfile(
  RequestContext context,
  ProfileRepository repo,
  String userId,
) async {
  final body = await context.request.json() as Map&lt;String, dynamic&gt;;
  final profile = await repo.update(
    userId: userId,
    bio: body['bio'] as String?,
    avatarUrl: body['avatarUrl'] as String?,
    phone: body['phone'] as String?,
    location: body['location'] as String?,
    website: body['website'] as String?,
  );
  if (profile == null) {
    return Response.json(statusCode: 404, body: {'error': 'Profile not found'});
  }
  return Response.json(body: profile.toJson());
}
</code></pre>
<h2 id="heading-wiring-the-middleware-pipeline">Wiring the Middleware Pipeline</h2>
<p>The routes and middleware are all written, but they aren't connected yet. In Dart Frog, the connection happens through <code>_middleware.dart</code> files placed strategically in the routes/ folder.</p>
<p>To review, a <code>_middleware.dart</code> file at the root level applies to every route in the project. A <code>_middleware.dart</code> inside a subfolder applies only to routes in that folder and below. This gives us precise, folder-scoped control over which middleware runs where without any manual registration or mounting.</p>
<p>Create <code>routes/_middleware.dart</code> for global middleware applied to every route:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';
import '../lib/middleware/database_middleware.dart';
import '../lib/middleware/error_middleware.dart';

Handler middleware(Handler handler) {
  return handler
      .use(databaseMiddleware())
      .use(errorMiddleware());
}
</code></pre>
<p>Create <code>routes/users/_middleware.dart</code> to protect all user routes with authentication:</p>
<pre><code class="language-dart">import 'package:dart_frog/dart_frog.dart';
import '../../lib/middleware/auth_middleware.dart';

Handler middleware(Handler handler) {
  return handler.use(authMiddleware());
}
</code></pre>
<p>This is one of the most elegant parts of Dart Frog's model. The routes/users/_middleware.dart file automatically applies auth to every route under routes/users/, including routes/users/index.dart, routes/users/[id].dart, and routes/users/[id]/profile.dart. The auth routes under routes/auth/ are untouched because they live outside the users/ folder.</p>
<p>There's no manual middleware mounting, no array of protected routes, and no route group configuration. The folder structure does the work.</p>
<h2 id="heading-testing-the-api">Testing the API</h2>
<p>With the server running and all routes wired up, we can verify the full flow end to end. Start the development server and run through each endpoint in order: register a user first to get a token, then use that token on the protected routes. Replace {userId} in the commands below with the actual ID returned from the register response.</p>
<p>Start the development server:</p>
<pre><code class="language-bash">dart_frog dev
# Server is now running at: http://localhost:8080
</code></pre>
<p>Register a user:</p>
<pre><code class="language-bash">curl http://localhost:8080/auth/register \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "email": "seyi@example.com",
    "password": "securepassword",
    "firstName": "Seyi",
    "lastName": "Dev"
  }'
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{
  "user": {
    "id": "uuid-here",
    "email": "seyi@example.com",
    "firstName": "Seyi",
    "lastName": "Dev",
    "isActive": true,
    "createdAt": "2025-01-01T00:00:00.000Z",
    "updatedAt": "2025-01-01T00:00:00.000Z"
  },
  "token": "eyJhbGci..."
}
</code></pre>
<p>Login:</p>
<pre><code class="language-bash">curl http://localhost:8080/auth/login \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "seyi@example.com", "password": "securepassword"}'
</code></pre>
<p>Get all users:</p>
<pre><code class="language-bash">curl http://localhost:8080/users \
  -H "Authorization: Bearer eyJhbGci..."
</code></pre>
<p>Get a specific user:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId} \
  -H "Authorization: Bearer eyJhbGci..."
</code></pre>
<p>Create a profile:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId}/profile \
  -X POST \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{
    "bio": "Flutter engineer turned backend developer",
    "location": "Lagos, Nigeria",
    "website": "https://example.com"
  }'
</code></pre>
<p>Update a user:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId} \
  -X PUT \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{"firstName": "Oluwaseyi"}'
</code></pre>
<p>Delete a user:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId} \
  -X DELETE \
  -H "Authorization: Bearer eyJhbGci..."
</code></pre>
<h2 id="heading-deployment">Deployment</h2>
<p>With everything tested locally, the final step is getting the API live. Dart Frog makes this straightforward: a single CLI command generates a production-ready Dockerfile, and from there we deploy to Fly.io where the app will run as a containerized service alongside a managed PostgreSQL database.</p>
<h3 id="heading-production-build">Production Build</h3>
<p>Dart Frog generates a production-ready Docker setup with a single command:</p>
<pre><code class="language-bash">dart_frog build
</code></pre>
<p>This creates a build/ directory containing:</p>
<pre><code class="language-plaintext">build/
  bin/
    server.dart         ← compiled entry point
  Dockerfile            ← production Dockerfile
  pubspec.yaml
  pubspec.lock
</code></pre>
<p>The generated Dockerfile is a multi-stage build, compiles to a native binary in the first stage, runs from a minimal Debian image in the second. You do not need to write this yourself.</p>
<h3 id="heading-deploying-to-flyio">Deploying to Fly.io</h3>
<p><strong>Step 1 — Authenticate:</strong></p>
<pre><code class="language-bash">fly auth login
</code></pre>
<p><strong>Step 2 — Launch from the build directory:</strong></p>
<pre><code class="language-bash">cd build
fly launch
</code></pre>
<p>Fly detects the Dockerfile and prompts for configuration. Create a PostgreSQL database when asked.</p>
<p><strong>Step 3 — Set secrets:</strong></p>
<pre><code class="language-bash">fly secrets set JWT_SECRET="your_production_jwt_secret"
fly secrets set JWT_EXPIRY_HOURS="24"
</code></pre>
<p><strong>Step 4 — Deploy:</strong></p>
<pre><code class="language-bash">fly deploy
</code></pre>
<p><strong>Step 5 — Verify:</strong></p>
<pre><code class="language-bash">curl https://your-app-name.fly.dev/auth/register \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password123","firstName":"Seyi","lastName":"Dev"}'
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Dart Frog sits exactly where it positions itself: between the raw control of Shelf and the full opinions of Serverpod. It takes the file-based routing model that has proven itself in the JavaScript ecosystem and brings it to Dart cleanly, without compromising on the language's strengths.</p>
<p>The routing model is its strongest feature. Looking at the routes/ folder tells you everything about your API: what endpoints exist, how they are grouped, and which middleware applies to which sections. That transparency makes codebases easier to navigate, easier to onboard into, and easier to reason about as they grow.</p>
<p>The RequestContext and the provider pattern for dependency injection are well thought out. Middleware injects, routes consume, and nothing bleeds between the two. The folder-scoped middleware is particularly clean, protecting an entire section of your API is as simple as dropping a _middleware.dart file in the right folder.</p>
<p>For Flutter engineers building APIs that need to serve multiple client types, conform to standard REST conventions, or integrate cleanly with existing frontend infrastructure, Dart Frog hits a practical sweet spot that neither Shelf nor Serverpod reaches as naturally.</p>
<p>Dart is now a full-stack language in the truest sense. The same team, the same language, the same conventions – from the Flutter app to the server that powers it.</p>
<p>Happy Coding!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
