<?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[ backend developments - 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[ backend developments - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 26 Jul 2026 22:33:20 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/backend-developments/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ From Flutter to Backend: How to Build and Ship Production REST APIs with Dart and Shelf ]]>
                </title>
                <description>
                    <![CDATA[ As a Flutter engineer, you already know Dart. You understand async/await, you work with models and repositories, you think in clean architecture, and you have shipped real applications. The gap betwee ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-and-ship-production-rest-apis-with-dart-and-shelf/</link>
                <guid isPermaLink="false">6a1d92fa080b80f11f574194</guid>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend developments ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ REST API ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Mon, 01 Jun 2026 14:11:06 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8ba5ec9d-22ba-4313-9b34-ce1e0e7dce23.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As a Flutter engineer, you already know Dart. You understand async/await, you work with models and repositories, you think in clean architecture, and you have shipped real applications.</p>
<p>The gap between where you are and being able to build and deploy a production backend is smaller than you think.</p>
<p>The missing piece is not a new language. It's not a new paradigm. It's understanding how Dart behaves when there's no widget tree, no BuildContext, no Flutter framework – just a running process handling HTTP requests, talking to a database, and sending responses back to clients.</p>
<p>That's exactly what this article covers.</p>
<p>We're going to build a full User and Profile Management REST API from scratch using Dart and Shelf, connect it to a PostgreSQL database running in Docker, secure it with JWT authentication, and deploy it to Fly.io.</p>
<p>By the end, you'll have a working production-grade backend written entirely in Dart, the same language you already know.</p>
<p>This article is part of a series (of standalone articles) where we'll build the same project using three different frameworks. We'll use Shelf here, Serverpod in the next article, and Dart Frog in the one after that. This will let you directly compare how each framework approaches the same problem.</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-works-on-the-server">How Dart Works on the Server</a></p>
</li>
<li><p><a href="#heading-what-is-shelf">What is Shelf?</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
<ul>
<li><p><a href="#heading-creating-the-project">Creating the Project</a></p>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-database-setup-with-docker">Database Setup with Docker</a></p>
</li>
<li><p><a href="#heading-environment-configuration">Environment Configuration</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-shelf-core-concepts">Shelf Core Concepts</a></p>
<ul>
<li><p><a href="#heading-handlers">Handlers</a></p>
</li>
<li><p><a href="#heading-request-and-response">Request and Response</a></p>
</li>
<li><p><a href="#heading-router">Router</a></p>
</li>
<li><p><a href="#heading-pipeline-and-middleware">Pipeline and Middleware</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-connecting-to-postgresql">Connecting to PostgreSQL</a></p>
<ul>
<li><p><a href="#heading-the-database-connection-manager">The Database Connection Manager</a></p>
</li>
<li><p><a href="#heading-running-migrations">Running Migrations</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-building-the-api">Building the API</a></p>
<ul>
<li><p><a href="#heading-the-user-model">The User Model</a></p>
</li>
<li><p><a href="#heading-the-user-repository">The User Repository</a></p>
</li>
<li><p><a href="#heading-user-handlers">User Handlers</a></p>
</li>
<li><p><a href="#heading-the-profile-model">The Profile Model</a></p>
</li>
<li><p><a href="#heading-the-profile-repository">The Profile Repository</a></p>
</li>
<li><p><a href="#heading-profile-handlers">Profile Handlers</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-authentication">Authentication</a></p>
<ul>
<li><p><a href="#heading-password-hashing">Password Hashing</a></p>
</li>
<li><p><a href="#heading-jwt-token-generation-and-validation">JWT Token Generation and Validation</a></p>
</li>
<li><p><a href="#heading-auth-handlers">Auth Handlers</a></p>
</li>
<li><p><a href="#heading-auth-middleware">Auth Middleware</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-error-handling">Error Handling</a></p>
</li>
<li><p><a href="#heading-wiring-everything-together">Wiring Everything Together</a></p>
</li>
<li><p><a href="#heading-deployment">Deployment</a></p>
<ul>
<li><p><a href="#heading-dockerfile">Dockerfile</a></p>
</li>
<li><p><a href="#heading-docker-compose-for-local-production-testing">Docker Compose for Local Production Testing</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-testing-the-api">Testing the API</a></p>
</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 (free tier is sufficient, fly.io)</p>
</li>
<li><p>The Fly CLI installed (brew install flyctl on macOS, or the official installer on Windows/Linux)</p>
</li>
<li><p>A PostgreSQL client for inspecting the database, like TablePlus or DBeaver – both work well</p>
</li>
</ul>
<h2 id="heading-how-dart-works-on-the-server">How Dart Works on the Server</h2>
<p>When you run a Flutter app, the Flutter framework is doing an enormous amount of work, managing the widget tree, handling the render pipeline, coordinating state, and responding to platform events. Your Dart code sits on top of all of that.</p>
<p>On the server, none of that exists. There's no widget tree. There's no framework managing a UI lifecycle. There's just a Dart process running, listening on a port, receiving HTTP requests, doing work, and sending responses.</p>
<p>Dart's standard library, dart:io, has everything needed to do this at the lowest level:</p>
<pre><code class="language-dart">import 'dart:io';

void main() async {
  final server = await HttpServer.bind('0.0.0.0', 8080);
  print('Server running on port 8080');

  await for (final request in server) {
    request.response
      ..statusCode = 200
      ..write('Hello from Dart')
      ..close();
  }
}
</code></pre>
<p>This is a working HTTP server in raw Dart. No packages, no framework. Every request comes in through the HttpServer stream, and you write directly to the response.</p>
<p>This works, but it scales poorly. As soon as you need routing, middleware, authentication, and structured error handling, raw dart:io becomes difficult to manage. That is the problem Shelf solves.</p>
<h2 id="heading-what-is-shelf">What is Shelf?</h2>
<p>Shelf is a composable web server middleware library for Dart, maintained by the Dart team. It doesn't try to be a full framework – instead, it gives you the primitives to build one, or to assemble exactly what you need.</p>
<p>The Shelf mental model is built on four concepts:</p>
<ul>
<li><p><strong>Handler:</strong> a function that takes a Request and returns a Response. Everything in Shelf is ultimately a handler.</p>
</li>
<li><p><strong>Middleware:</strong> a function that wraps a handler, adding behaviour before or after it runs. Logging, authentication, and error handling are all middleware.</p>
</li>
<li><p><strong>Pipeline:</strong> a chain of middleware with a handler at the end. Requests flow through the middleware chain before reaching the handler.</p>
</li>
<li><p><strong>Router:</strong> maps URL patterns and HTTP methods to specific handlers.</p>
</li>
</ul>
<p>If you've used Flutter's Navigator or provider middleware concepts, the composition model will feel familiar. Small, single-responsibility pieces assembled into a working whole.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<h3 id="heading-creating-the-project">Creating the Project</h3>
<p>Dart includes a server-side project template that gives us a clean starting point:</p>
<pre><code class="language-bash">dart create -t server-shelf user_profile_api
cd user_profile_api
</code></pre>
<p>Add the dependencies we need to pubspec.yaml:</p>
<pre><code class="language-yaml">name: user_profile_api
description: User and Profile Management REST API built with Dart and Shelf
version: 1.0.0

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

dependencies:
  shelf: ^1.4.1
  shelf_router: ^1.1.4
  postgres: ^3.3.0
  dart_jsonwebtoken: ^2.12.0
  bcrypt: ^1.1.3
  dotenv: ^4.1.0
  crypto: ^3.0.3

dev_dependencies:
  lints: ^3.0.0
  test: ^1.24.0
</code></pre>
<p>Run:</p>
<pre><code class="language-bash">dart pub get
</code></pre>
<h3 id="heading-project-structure">Project Structure</h3>
<p>Now we'll build a backend project structure that Flutter engineers will find intuitive, that's familiar enough to navigate immediately, and that's correct enough for backend conventions:</p>
<pre><code class="language-plaintext">user_profile_api/
  bin/
    server.dart              ← entry point
  lib/
    config/
      database.dart          ← connection manager
      env.dart               ← environment config
    handlers/
      auth_handler.dart      ← auth endpoints
      user_handler.dart      ← user endpoints
      profile_handler.dart   ← profile endpoints
    middleware/
      auth_middleware.dart   ← JWT validation
      error_middleware.dart  ← global error handling
      logger_middleware.dart ← request logging
    models/
      user.dart
      profile.dart
    repositories/
      user_repository.dart
      profile_repository.dart
    services/
      auth_service.dart      ← JWT + password logic
    router.dart              ← route definitions
  migrations/
    001_create_users.sql
    002_create_profiles.sql
  docker-compose.yml
  Dockerfile
  .env
  .env.example
</code></pre>
<p>This separation of concerns maps directly to what you'll already know if you're a Flutter engineer: models, repositories, and services are the same concepts. Handlers replace ViewModels or Controllers. Middleware replaces interceptors.</p>
<h3 id="heading-database-setup-with-docker">Database Setup with Docker</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

volumes:
  postgres_data:
</code></pre>
<p>Start the database:</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<p>Verify that it's running:</p>
<pre><code class="language-bash">docker compose ps
# user_profile_db   running   0.0.0.0:5432-&gt;5432/tcp
</code></pre>
<h3 id="heading-environment-configuration">Environment Configuration</h3>
<p>Create .env in the project root:</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 .env.example with the same keys but no values. This is what you commit to Git:</p>
<pre><code class="language-plaintext">DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
JWT_SECRET=
JWT_EXPIRY_HOURS=
PORT=
</code></pre>
<p>Add .env to .gitignore:</p>
<pre><code class="language-plaintext">.env
</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');
  static int get port =&gt; int.parse(_env['PORT'] ?? '8080');
}
</code></pre>
<p>includePlatformEnvironment: true means the Env class reads from both the .env file and real system environment variables, so the same code works locally with a .env file and in production with injected environment variables.</p>
<h2 id="heading-shelf-core-concepts">Shelf Core Concepts</h2>
<p>Before building the API, it's worth understanding each Shelf concept properly – not just what it does, but why it's designed the way it is.</p>
<h3 id="heading-handlers">Handlers</h3>
<p>A handler is the most fundamental unit in Shelf. It's simply a function:</p>
<pre><code class="language-dart">import 'package:shelf/shelf.dart';

Response helloHandler(Request request) {
  return Response.ok('Hello, Dart backend!');
}
</code></pre>
<p>Request in, Response out. That's the entire contract. Every endpoint you write is a handler. Every piece of middleware is a function that takes a handler and returns a handler.</p>
<p>Handlers can be async:</p>
<pre><code class="language-dart">Future&lt;Response&gt; getUserHandler(Request request) async {
  final users = await userRepository.findAll();
  return Response.ok(jsonEncode(users));
}
</code></pre>
<h3 id="heading-request-and-response">Request and Response</h3>
<p>Request gives you everything about the incoming HTTP call:</p>
<pre><code class="language-dart">Future&lt;Response&gt; handler(Request request) async {
  // URL and path
  print(request.url);           // the full URL
  print(request.url.path);      // just the path

  // Path parameters (when using shelf_router)
  final id = request.params['id'];

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

  // Headers
  final auth = request.headers['authorization'];

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

  return Response.ok('handled');
}
</code></pre>
<p>Response has named constructors for common status codes:</p>
<pre><code class="language-dart">Response.ok(body)           // 200
Response.notFound(body)     // 404
Response(201, body: body)   // any status code
Response(400, body: body)   // bad request
Response(401, body: body)   // unauthorized
Response(500, body: body)   // server error
</code></pre>
<p>Always set the Content-Type header when returning JSON:</p>
<pre><code class="language-dart">Response.ok(
  jsonEncode({'message': 'success'}),
  headers: {'Content-Type': 'application/json'},
)
</code></pre>
<h3 id="heading-router">Router</h3>
<p>shelf_router maps URL patterns and HTTP methods to handlers:</p>
<pre><code class="language-dart">import 'package:shelf_router/shelf_router.dart';

final router = Router();

router.get('/users', getAllUsersHandler);
router.get('/users/&lt;id&gt;', getUserHandler);
router.post('/users', createUserHandler);
router.put('/users/&lt;id&gt;', updateUserHandler);
router.delete('/users/&lt;id&gt;', deleteUserHandler);
</code></pre>
<p>The syntax defines a path parameter. Access it inside the handler via request.params['id'].</p>
<h3 id="heading-pipeline-and-middleware">Pipeline and Middleware</h3>
<p>A Pipeline chains middleware together with a handler at the end:</p>
<pre><code class="language-dart">import 'package:shelf/shelf.dart';

final handler = Pipeline()
    .addMiddleware(loggerMiddleware())
    .addMiddleware(errorMiddleware())
    .addMiddleware(authMiddleware())
    .addHandler(router.call);
</code></pre>
<p>Middleware is a function with this signature:</p>
<pre><code class="language-dart">Middleware myMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      // Before the handler runs
      print('Request received: \({request.method} \){request.url}');

      final response = await innerHandler(request);

      // After the handler runs
      print('Response sent: ${response.statusCode}');

      return response;
    };
  };
}
</code></pre>
<p>The outer function returns a Middleware. That Middleware is a function that takes the next Handler in the chain and returns a new Handler. This nesting is what allows middleware to run code both before and after the inner handler.</p>
<h2 id="heading-connecting-to-postgresql">Connecting to PostgreSQL</h2>
<h3 id="heading-the-database-connection-manager">The 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 _connect();
    return _connection!;
  }

  static Future&lt;Connection&gt; _connect() async {
    final conn = 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: \({Env.dbHost}:\){Env.dbPort}/${Env.dbName}');
    return conn;
  }

  static Future&lt;void&gt; close() async {
    await _connection?.close();
    _connection = null;
  }
}
</code></pre>
<p>This is a singleton connection manager – the same pattern Flutter engineers use for shared services. The connection is created once on first access and reused for every subsequent database call.</p>
<h3 id="heading-running-migrations">Running Migrations</h3>
<p>Create the migrations folder and SQL files:</p>
<p>migrations/001_create_users.sql:</p>
<pre><code class="language-sql">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);
</code></pre>
<p>migrations/002_create_profiles.sql:</p>
<pre><code class="language-sql">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);
</code></pre>
<p>Create a migration runner in lib/config/database.dart:</p>
<pre><code class="language-dart">static Future&lt;void&gt; runMigrations() async {
  final conn = await connection;
  final migrationsDir = Directory('migrations');

  final files = migrationsDir
      .listSync()
      .whereType&lt;File&gt;()
      .where((f) =&gt; f.path.endsWith('.sql'))
      .toList()
    ..sort((a, b) =&gt; a.path.compareTo(b.path));

  for (final file in files) {
    final sql = await file.readAsString();
    await conn.execute(sql);
    print('✅ Migration applied: ${file.path}');
  }
}
</code></pre>
<h2 id="heading-building-the-api">Building the API</h2>
<p>With the database connected and migrations in place, we can now build the actual API layer.</p>
<p>This section covers the models, repositories, and handlers for both users and profiles. Models define the shape of the data, repositories handle all database interactions, and handlers translate HTTP requests into repository calls and send responses back to the client. We'll build the user layer first, then the profile layer on top of it.</p>
<h3 id="heading-the-user-model">The User Model</h3>
<p>The User model represents a single user record in the database. It maps directly to the users table created in the migration and handles two-way conversion between database rows and Dart objects.</p>
<p>Create lib/models/user.dart:</p>
<pre><code class="language-dart">class User {
  final String id;
  final String email;
  final String passwordHash;
  final String firstName;
  final String lastName;
  final bool isActive;
  final DateTime createdAt;
  final DateTime updatedAt;

  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,
  });

  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,
      );

  // Never include passwordHash in JSON responses
  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>fromRow maps a PostgreSQL result row to a User. toJson deliberately excludes passwordHash – you should never return password data in API responses.</p>
<h3 id="heading-the-user-repository">The User Repository</h3>
<p>The UserRepository is the single point of contact between the application and the users table. Every database operation for users goes through here, keeping the SQL contained and the handlers clean.</p>
<p>Create lib/repositories/user_repository.dart:</p>
<pre><code class="language-dart">import 'dart:async';
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((row) =&gt; User.fromRow(row.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>
<p>A few things worth noting here. Sql.named uses named parameters (@paramName) instead of positional parameters. This prevents SQL injection and makes queries readable.</p>
<p>Also, the delete operation is a soft delete. It sets is_active = FALSE rather than removing the row. This is the standard production approach: data is never truly deleted, it's deactivated.</p>
<p>COALESCE(@firstName, first_name) on the update means: use the new value if provided, otherwise keep the existing value. This handles partial updates cleanly without requiring all fields every time.</p>
<h3 id="heading-user-handlers">User Handlers</h3>
<p>The UserHandler class exposes the repository operations as HTTP endpoints. It owns a Router instance internally and maps each route to a private method, keeping the routing logic and the handler logic together in one place.</p>
<p>Create lib/handlers/user_handler.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/user_repository.dart';

class UserHandler {
  final UserRepository _repository;

  UserHandler(this._repository);

  Router get router {
    final router = Router();
    router.get('/', _getAll);
    router.get('/&lt;id&gt;', _getOne);
    router.put('/&lt;id&gt;', _update);
    router.delete('/&lt;id&gt;', _delete);
    return router;
  }

  Future&lt;Response&gt; _getAll(Request request) async {
    final users = await _repository.findAll();
    return Response.ok(
      jsonEncode(users.map((u) =&gt; u.toJson()).toList()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _getOne(Request request, String id) async {
    final user = await _repository.findById(id);

    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(user.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _update(Request request, String id) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final user = await _repository.update(
      id: id,
      firstName: body['firstName'] as String?,
      lastName: body['lastName'] as String?,
    );

    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(user.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _delete(Request request, String id) async {
    final deleted = await _repository.delete(id);

    if (!deleted) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response(
      204,
      headers: {'Content-Type': 'application/json'},
    );
  }
}
</code></pre>
<h3 id="heading-the-profile-model">The Profile Model</h3>
<p>The Profile model represents a user's extended information, stored separately from the core user record. The one-to-one relationship is enforced by the unique index on user_id in the profiles table. All fields except userId are nullable since a profile can be created with partial information and filled in over time.</p>
<p>Create lib/models/profile.dart:</p>
<pre><code class="language-dart">class Profile {
  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;

  const Profile({
    required this.id,
    required this.userId,
    this.bio,
    this.avatarUrl,
    this.phone,
    this.location,
    this.website,
    required this.createdAt,
    required this.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>
<h3 id="heading-the-profile-repository">The Profile Repository</h3>
<p>The ProfileRepository handles all database operations for the profiles table. Unlike the user repository which looks up by id, most profile operations use userId as the lookup key since that is how the client references a profile — by whose it belongs to, not by its own internal ID.</p>
<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>
<h3 id="heading-profile-handlers">Profile Handlers</h3>
<p>The ProfileHandler manages the profile endpoints nested under a user's ID. Before every operation, it verifies the parent user exists — a profile can't be created, fetched, or updated for a user that doesn't exist. It also prevents duplicate profiles by checking for an existing record before allowing a create.</p>
<p>Create lib/handlers/profile_handler.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/profile_repository.dart';
import '../repositories/user_repository.dart';

class ProfileHandler {
  final ProfileRepository _profileRepository;
  final UserRepository _userRepository;

  ProfileHandler(this._profileRepository, this._userRepository);

  Router get router {
    final router = Router();
    router.get('/&lt;userId&gt;/profile', _getProfile);
    router.post('/&lt;userId&gt;/profile', _createProfile);
    router.put('/&lt;userId&gt;/profile', _updateProfile);
    return router;
  }

  Future&lt;Response&gt; _getProfile(Request request, String userId) async {
    final user = await _userRepository.findById(userId);
    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final profile = await _profileRepository.findByUserId(userId);
    if (profile == null) {
      return Response.notFound(
        jsonEncode({'error': 'Profile not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(profile.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _createProfile(Request request, String userId) async {
    final user = await _userRepository.findById(userId);
    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final existing = await _profileRepository.findByUserId(userId);
    if (existing != null) {
      return Response(
        409,
        body: jsonEncode({'error': 'Profile already exists for this user'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final profile = await _profileRepository.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(
      201,
      body: jsonEncode(profile.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _updateProfile(Request request, String userId) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final profile = await _profileRepository.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.notFound(
        jsonEncode({'error': 'Profile not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(profile.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }
}
</code></pre>
<h2 id="heading-authentication">Authentication</h2>
<p>With the core user and profile CRUD in place, the next step is securing the API.</p>
<p>Authentication in this project works in two parts: an AuthService handles the cryptographic operations — password hashing and JWT generation and verification — and an AuthHandler exposes the register and login endpoints that clients call to get a token. Once a token is issued, the AuthMiddleware validates it on every protected request before it reaches a handler.</p>
<h3 id="heading-password-hashing">Password Hashing</h3>
<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) {
    return BCrypt.hashpw(password, BCrypt.gensalt());
  }

  bool verifyPassword(String password, String hash) {
    return 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>
<p>BCrypt.hashpw generates a salted hash. BCrypt.checkpw verifies a plain password against a stored hash. The salt is embedded in the hash itself – you don't store it separately.</p>
<p>verifyToken returns null on any failure, expired token, invalid signature, or malformed token rather than throwing. This keeps the auth middleware clean.</p>
<h3 id="heading-auth-handlers">Auth Handlers</h3>
<p>Create lib/handlers/auth_handler.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/user_repository.dart';
import '../services/auth_service.dart';

class AuthHandler {
  final UserRepository _userRepository;
  final AuthService _authService;

  AuthHandler(this._userRepository, this._authService);

  Router get router {
    final router = Router();
    router.post('/register', _register);
    router.post('/login', _login);
    return router;
  }

  Future&lt;Response&gt; _register(Request request) async {
    final body = jsonDecode(await request.readAsString()) 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(
        400,
        body: jsonEncode({'error': 'email, password, firstName, and lastName are required'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    if (password.length &lt; 8) {
      return Response(
        400,
        body: jsonEncode({'error': 'Password must be at least 8 characters'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final existing = await _userRepository.findByEmail(email);
    if (existing != null) {
      return Response(
        409,
        body: jsonEncode({'error': 'An account with this email already exists'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final passwordHash = _authService.hashPassword(password);

    final user = await _userRepository.create(
      email: email,
      passwordHash: passwordHash,
      firstName: firstName,
      lastName: lastName,
    );

    final token = _authService.generateToken(user);

    return Response(
      201,
      body: jsonEncode({
        'user': user.toJson(),
        'token': token,
      }),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _login(Request request) async {
    final body = jsonDecode(await request.readAsString()) 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(
        400,
        body: jsonEncode({'error': 'email and password are required'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final user = await _userRepository.findByEmail(email);

    // Deliberately vague error, never confirm whether an email exists
    if (user == null || !_authService.verifyPassword(password, user.passwordHash)) {
      return Response(
        401,
        body: jsonEncode({'error': 'Invalid email or password'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final token = _authService.generateToken(user);

    return Response.ok(
      jsonEncode({
        'user': user.toJson(),
        'token': token,
      }),
      headers: {'Content-Type': 'application/json'},
    );
  }
}
</code></pre>
<p>The login error message is deliberately vague: "Invalid email or password" rather than "Email not found" or "Wrong password." Confirming which part is wrong helps attackers enumerate valid accounts.</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:shelf/shelf.dart';
import '../services/auth_service.dart';

Middleware authMiddleware(AuthService authService) {
  return (Handler innerHandler) {
    return (Request request) async {
      final authHeader = request.headers['authorization'];

      if (authHeader == null || !authHeader.startsWith('Bearer ')) {
        return Response(
          401,
          body: jsonEncode({'error': 'Authorization header missing or malformed'}),
          headers: {'Content-Type': 'application/json'},
        );
      }

      final token = authHeader.substring(7); // Remove 'Bearer '
      final jwt = authService.verifyToken(token);

      if (jwt == null) {
        return Response(
          401,
          body: jsonEncode({'error': 'Invalid or expired token'}),
          headers: {'Content-Type': 'application/json'},
        );
      }

      // Attach the user ID to the request context for downstream handlers
      final updatedRequest = request.change(
        context: {
          ...request.context,
          'userId': jwt.payload['sub'] as String,
          'userEmail': jwt.payload['email'] as String,
        },
      );

      return innerHandler(updatedRequest);
    };
  };
}
</code></pre>
<p>request.change(context: {...}) is how Shelf passes data from middleware to handlers, the equivalent of attaching data to a request in Express or ASP.NET middleware. Any handler downstream can read request.context['userId'] to know which user is authenticated.</p>
<h2 id="heading-error-handling">Error Handling</h2>
<p>No matter how carefully you write your handlers, unexpected failures will happen in production — malformed request bodies, database timeouts, unhandled edge cases.</p>
<p>Rather than letting each handler manage its own error responses individually, we'll centralise error handling in a single middleware that wraps the entire pipeline. This guarantees a consistent error response shape across every endpoint and prevents internal error details from leaking to the client.</p>
<p>Create lib/middleware/error_middleware.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';

Middleware errorMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      try {
        return await innerHandler(request);
      } on FormatException catch (e) {
        return Response(
          400,
          body: jsonEncode({'error': 'Invalid request body: ${e.message}'}),
          headers: {'Content-Type': 'application/json'},
        );
      } catch (e, stackTrace) {
        // Log the full error and stack trace server-side
        print('Unhandled error: $e');
        print(stackTrace);

        // Never expose internal error details to the client
        return Response(
          500,
          body: jsonEncode({'error': 'An internal server error occurred'}),
          headers: {'Content-Type': 'application/json'},
        );
      }
    };
  };
}
</code></pre>
<p>Create lib/middleware/logger_middleware.dart:</p>
<pre><code class="language-dart">import 'package:shelf/shelf.dart';

Middleware loggerMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      final start = DateTime.now();

      final response = await innerHandler(request);

      final duration = DateTime.now().difference(start).inMilliseconds;
      print(
        '[${DateTime.now().toIso8601String()}] '
        '\({request.method} \){request.url.path} '
        '→ \({response.statusCode} (\){duration}ms)',
      );

      return response;
    };
  };
}
</code></pre>
<h2 id="heading-wiring-everything-together">Wiring Everything Together</h2>
<p>With the handlers, repositories, and middleware all in place, the final step is connecting them into a single running server. The router maps URL prefixes to their handler, the pipeline stacks the middleware in the correct order, and the entry point boots everything up in sequence — loading environment variables, running migrations, and starting the server.</p>
<p>Create lib/router.dart:</p>
<pre><code class="language-dart">import 'package:shelf_router/shelf_router.dart';
import 'handlers/auth_handler.dart';
import 'handlers/user_handler.dart';
import 'handlers/profile_handler.dart';
import 'middleware/auth_middleware.dart';
import 'repositories/user_repository.dart';
import 'repositories/profile_repository.dart';
import 'services/auth_service.dart';

Router createRouter() {
  final userRepository = UserRepository();
  final profileRepository = ProfileRepository();
  final authService = AuthService();

  final authHandler = AuthHandler(userRepository, authService);
  final userHandler = UserHandler(userRepository);
  final profileHandler = ProfileHandler(profileRepository, userRepository);

  final router = Router();

  // Public routes, no auth required
  router.mount('/auth', authHandler.router.call);

  // Protected routes, auth middleware applied
  router.mount(
    '/users',
    Pipeline()
        .addMiddleware(authMiddleware(authService))
        .addHandler(userHandler.router.call),
  );

  router.mount(
    '/users',
    Pipeline()
        .addMiddleware(authMiddleware(authService))
        .addHandler(profileHandler.router.call),
  );

  return router;
}
</code></pre>
<p>Create the entry point bin/server.dart:</p>
<pre><code class="language-dart">import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import '../lib/config/database.dart';
import '../lib/config/env.dart';
import '../lib/middleware/error_middleware.dart';
import '../lib/middleware/logger_middleware.dart';
import '../lib/router.dart';

void main() async {
  // Load environment variables
  Env.load();

  // Run database migrations
  await Database.runMigrations();

  // Build the handler pipeline
  final router = createRouter();

  final handler = Pipeline()
      .addMiddleware(errorMiddleware())
      .addMiddleware(loggerMiddleware())
      .addHandler(router.call);

  // Start the server
  final server = await shelf_io.serve(
    handler,
    InternetAddress.anyIPv4,
    Env.port,
  );

  print('🚀 Server running on port ${server.port}');
}
</code></pre>
<p>Run the server:</p>
<pre><code class="language-bash">dart run bin/server.dart
# ✅ Database connected: localhost:5432/user_profile_api
# ✅ Migration applied: migrations/001_create_users.sql
# ✅ Migration applied: migrations/002_create_profiles.sql
# 🚀 Server running on port 8080
</code></pre>
<h2 id="heading-deployment">Deployment</h2>
<p>The server is running locally and all endpoints are working. Now it's time to ship it.</p>
<p>We'll cover two deployment paths: first packaging the app and database together with Docker Compose for local production testing, then deploying to Fly.io where your API will be accessible over the internet with a managed PostgreSQL database and automatic TLS.</p>
<h3 id="heading-dockerfile">Dockerfile</h3>
<p>Create Dockerfile in the project root:</p>
<pre><code class="language-dockerfile">FROM dart:stable AS build

WORKDIR /app
COPY pubspec.* ./
RUN dart pub get

COPY . .
RUN dart compile exe bin/server.dart -o bin/server

FROM debian:stable-slim

RUN apt-get update &amp;&amp; apt-get install -y ca-certificates &amp;&amp; rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=build /app/bin/server bin/server
COPY --from=build /app/migrations migrations/

EXPOSE 8080

CMD ["bin/server"]
</code></pre>
<p>This is a multi-stage build. The first stage uses the full Dart SDK image to compile the server to a native binary. The second stage copies only the compiled binary and migrations into a minimal Debian image – no Dart SDK, no source code, no build tools. The final image is lean and production-ready.</p>
<h3 id="heading-docker-compose-for-local-production-testing">Docker Compose for Local Production Testing</h3>
<p>Update docker-compose.yml to include the app alongside the database:</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

  api:
    build: .
    container_name: user_profile_api
    ports:
      - "8080:8080"
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: user_profile_api
      DB_USER: dart_user
      DB_PASSWORD: dart_password
      JWT_SECRET: local_test_secret_replace_in_production
      JWT_EXPIRY_HOURS: 24
      PORT: 8080
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:
</code></pre>
<p>The healthcheck on the Postgres service ensures that the API container only starts once the database is ready to accept connections (a common production problem when services start simultaneously).</p>
<p>Build and run everything:</p>
<pre><code class="language-bash">docker compose up --build
</code></pre>
<h3 id="heading-deploying-to-flyio">Deploying to Fly.io</h3>
<p>Fly.io is one of the cleanest deployment targets for containerized backend services. It handles global distribution, automatic TLS, and managed PostgreSQL databases.</p>
<p><strong>Step 1 – Install and authenticate:</strong></p>
<pre><code class="language-bash"># macOS
brew install flyctl

# Authenticate
fly auth login
</code></pre>
<p><strong>Step 2 – Launch the app:</strong></p>
<pre><code class="language-bash">fly launch
</code></pre>
<p>Fly detects the Dockerfile automatically and asks a few questions: app name, region, and whether to create a PostgreSQL database. Answer yes to the PostgreSQL prompt, and Fly will provision a managed database and inject the connection string automatically.</p>
<p><strong>Step 3 – Set environment variables:</strong></p>
<pre><code class="language-bash">fly secrets set JWT_SECRET="your_production_secret_here"
fly secrets set JWT_EXPIRY_HOURS="24"
</code></pre>
<p>Database connection variables are set automatically by Fly when it provisions the PostgreSQL cluster.</p>
<p><strong>Step 4 – Deploy:</strong></p>
<pre><code class="language-bash">fly deploy
</code></pre>
<p>Fly builds the Docker image, pushes it to their registry, and deploys it to your chosen region. Once complete:</p>
<pre><code class="language-bash">fly status
# Your app is running at https://your-app-name.fly.dev
</code></pre>
<p><strong>Step 5 – Verify the deployment:</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-testing-the-api">Testing the API</h2>
<p>With the server running locally on port 8080, here's the full flow to verify that everything works end to end.</p>
<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 (authenticated):</p>
<pre><code class="language-bash">curl http://localhost:8080/users \
  -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-conclusion">Conclusion</h2>
<p>You just built and deployed a production-grade REST API in Dart – the same language you already know from Flutter. No new language, no new paradigm. Just Dart running in a different context.</p>
<p>The Shelf mental model (Handlers, Middleware, Pipelines, Routers) is deliberately minimal. It doesn't make decisions for you. It gives you composable primitives and lets you assemble them into exactly the architecture your project needs. That philosophy will feel familiar to Flutter engineers who build their own clean architecture rather than relying on a prescriptive framework.</p>
<p>What you built here – models, repositories, services, handlers, and middleware – is the same separation of concerns you apply in Flutter, applied to the backend. The concepts transfer. The Dart skills transfer. The architecture discipline transfers.</p>
<p>With this, you'll understand that Dart is a powerful language that cuts across both frontend and backend ecosystems. Aside from Shelf, we have Dartfrog and Serverpod which still functions well on the backend side of things. More on those in upcoming articles.</p>
<p>So yeah, try this out and thank me later!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Flexible API with Feature Flags Using Open Source Tools ]]>
                </title>
                <description>
                    <![CDATA[ Feature flagging has changed the paradigm of how backend developers can test and modify the things they build. With feature flags, we can enable and disable a feature or change the functionality of something on the fly with a single click (no need to... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-flexible-api-with-feature-flags-using-open-source-tools/</link>
                <guid isPermaLink="false">673d179a27c7af0d174dc2fc</guid>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Go Language ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Redis ]]>
                    </category>
                
                    <category>
                        <![CDATA[   feature flags ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend developments ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Pradumna Saraf ]]>
                </dc:creator>
                <pubDate>Tue, 19 Nov 2024 22:56:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732044691446/abd5596c-3523-4278-957c-109388690bcc.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Feature flagging has changed the paradigm of how backend developers can test and modify the things they build. With feature flags, we can enable and disable a feature or change the functionality of something on the fly with a single click (no need to redeploy).</p>
<p>In this tutorial, we will see how feature flags help us to enable and disable a feature/a part of code whenever we want from the UI, without the need to redeploy the whole code.</p>
<p>To understand things more deeply, we will build an app from scratch, look at feature flagging capabilities, and use a tool called Flagsmith to manage our created feature flags from a single dashboard.</p>
<h2 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-a-feature-flag">What is a Feature Flag?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-feature-flags-for-backend-development">Feature Flags for Backend Development</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-use-open-source-tools">Why Use Open Source Tools?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-lets-code">Let’s Code!</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-initializing-the-tools">Initializing the tools</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-endpoints-for-the-api">Creating endpoints for the API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-add-feature-flagging">How to Add Feature Flagging</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-feature-flag-code-logic">Understanding the Feature Flag code logic</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-feature-flags-in-the-flasgsmith-dashboard">How to Create Feature Flags in the Flasgsmith Dashboard</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-rate-limiting-feature-flag">Rate Limiting Feature Flag</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-beta-feature-flag">Beta Feature Flag</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-the-access-key">Getting the Access Key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-running-the-api">Running the API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-updating-the-ratelimit-flag">Updating the rate_limit Flag</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-integrate-feature-flags-with-the-github-app">How to Integrate Feature Flags with the GitHub App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-testing-the-flagsmith-github-app">Testing the Flagsmith GitHub App</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p><a target="_blank" href="https://go.dev/">Golang</a> installed and a medium-level understanding of it.</p>
</li>
<li><p>A running <a target="_blank" href="https://redis.io">Redis</a> instance (Remote or local instance)</p>
</li>
<li><p><a target="_blank" href="https://www.flagsmith.com/">Flagsmith</a> Account (It’s Free. We will cover this later in the article.)</p>
</li>
</ul>
<h2 id="heading-what-is-a-feature-flag">What is a Feature Flag?</h2>
<p>Feature Flag is a technique in development that allows teams to turn features on or off without modifying the source code or redeploying.</p>
<p>To make it a bit simpler, think of them as functioning sort of like conditional statements (for example, if-else statements): based on when something’s true or false, it determines the code path that will be executed.</p>
<h2 id="heading-feature-flags-for-backend-development">Feature Flags for Backend Development</h2>
<p>You may have seen feature flags used in frontends and websites, but there is much more to them. You can use them on the server side to modify the functionality of an API, doing things like modifying/setting the rate limit, changing the API endpoint's functionality or completely turning it off. As backend developers, we can level up our testing with feature flags.</p>
<p>To demonstrate this, we will go through building a demo app. The demo app is curated to show feature flagging capabilities from modifying the functionality (rate limit) on the fly to adding a new endpoint to the API for beta testing or initial rolling purposes. We’ll use entirely open-source tools along the way!</p>
<h2 id="heading-why-use-open-source-tools">Why Use Open Source Tools?</h2>
<p>We will be using open source tools to build this app (Golang, <a target="_blank" href="https://redis.io/">Redis</a>, and <a target="_blank" href="https://www.flagsmith.com/?utm_source=thirdparty&amp;utm_medium=freecodecamp&amp;utm_campaign=pradumna">Flagsmith</a>). Open source brings more transparency and trust and encourages collaboration with the global community of backend developers.</p>
<p>By integrating open source tools, we get full visibility as we build and test. For example, we will integrate feature flags with GitHub, which lets us track the lifecycle of a feature by linking a Flagsmith feature flag with a GitHub Pull Request or Issue. This lets us stay updated with the changes to our features without having to manually track each modification. We can easily track the status of our features across different environments.</p>
<h2 id="heading-lets-code">Let’s Code!</h2>
<p>In this tutorial, you’ll see how the functionality of an app changes before and after testing with feature flagging mechanisms. The tools and frameworks we’ll use are Golang, Docker, Redis, Flagsmith, and GitHub. As discussed, all are open source and free to create an account to test.</p>
<p>To get started, open your favourite IDE, initialize a Golang project, and then copy the below code in the <code>main.go</code> file. Then run <code>go mod tidy</code> to install all the dependencies it needs.</p>
<p>Let’s understand what’s going on in the below code snippet:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"errors"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"os"</span>
    <span class="hljs-string">"strconv"</span>

    <span class="hljs-string">"github.com/gin-gonic/gin"</span>
    <span class="hljs-string">"github.com/go-redis/redis_rate/v10"</span>
    <span class="hljs-string">"github.com/joho/godotenv"</span>
    <span class="hljs-string">"github.com/redis/go-redis/v9"</span>
)

<span class="hljs-keyword">var</span> (
    redisClient *redis.Client
    limiter     *redis_rate.Limiter
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">initClients</span><span class="hljs-params">()</span></span> {
    redisClient = redis.NewClient(&amp;redis.Options{
        Addr: os.Getenv(<span class="hljs-string">"REDIS_URL"</span>),
    })
    limiter = redis_rate.NewLimiter(redisClient)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    err := godotenv.Load()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Printf(<span class="hljs-string">"Loading environment variable from the host system"</span>)
    } <span class="hljs-keyword">else</span> {
        log.Printf(<span class="hljs-string">"Loading environment from .env file"</span>)
    }

    initClients()
    <span class="hljs-keyword">defer</span> redisClient.Close()

    r := gin.Default()
    r.GET(<span class="hljs-string">"/ping"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        err, remainingLimit := rateLimitCall(c.ClientIP())
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            c.JSON(
                http.StatusTooManyRequests,
                gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"Rate Limit Hit"</span>})
        } <span class="hljs-keyword">else</span> {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"Your left over API request is"</span>: remainingLimit})
        }
    })
    r.GET(<span class="hljs-string">"/beta"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        c.JSON(
            http.StatusOK,
            gin.H{<span class="hljs-string">"message"</span>: <span class="hljs-string">"This is beta endpoint"</span>})
    })
    r.Run(<span class="hljs-string">":"</span> + os.Getenv(<span class="hljs-string">"PORT"</span>))
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">rateLimitCall</span><span class="hljs-params">(ClientIP <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(error, <span class="hljs-keyword">int</span>)</span></span> {
    ctx := context.Background()

    rateLimitString := os.Getenv(<span class="hljs-string">"RATE_LIMIT"</span>)
    RATE_LIMIT, _ := strconv.Atoi(rateLimitString)

    res, err := limiter.Allow(ctx, ClientIP, redis_rate.PerHour(RATE_LIMIT))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    <span class="hljs-keyword">if</span> res.Remaining == <span class="hljs-number">0</span> {
        <span class="hljs-keyword">return</span> errors.New(<span class="hljs-string">"You have hit the Rate Limit for the API. Try again later"</span>), <span class="hljs-number">0</span>
    }

    fmt.Println(<span class="hljs-string">"remaining request for"</span>, ClientIP, <span class="hljs-string">"is"</span>, res.Remaining)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, res.Remaining
}
</code></pre>
<h3 id="heading-initializing-the-tools">Initializing the Tools</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">initClients</span><span class="hljs-params">()</span></span> {
    redisClient = redis.NewClient(&amp;redis.Options{
        Addr: os.Getenv(<span class="hljs-string">"REDIS_URL"</span>),
    })
    limiter = redis_rate.NewLimiter(redisClient)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    err := godotenv.Load()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Printf(<span class="hljs-string">"Loading environment variable from the host system"</span>)
    } <span class="hljs-keyword">else</span> {
        log.Printf(<span class="hljs-string">"Loading environment from .env file"</span>)
    }

    initClients()
    <span class="hljs-keyword">defer</span> redisClient.Close()

    r := gin.Default()
    ...
    })
</code></pre>
<p>At the top, we declare variables to store Redis and Rate limiter clients to reuse and initialise them once. Then we initialise them in the <code>initClients()</code>.</p>
<p>In <code>main()</code>, first, we load the environment variables from the system or the .env file. Then we call <code>initClients()</code>. This will create clients and store them in the variables we created.</p>
<p>Next, we create a <strong>Gin</strong> router that handles all our incoming requests. These are the environment variables we need in our <code>.env</code> file. For this demo, we need a Redis instance running to store all the data for rate-limiting functionality. We can use Docker or any remote machine – just remember to update <code>REDIS_URL</code> accordingly. I am going to use Docker.</p>
<p>We could also go a mile ahead and get all the environment variables from the feature flags, but we won’t do this here.</p>
<pre><code class="lang-bash">REDIS_URL=localhost:6379
PORT=8080
RATE_LIMIT=10
</code></pre>
<h3 id="heading-creating-endpoints-for-the-api">Creating Endpoints for the API</h3>
<pre><code class="lang-go">r.GET(<span class="hljs-string">"/ping"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        err, remainingLimit := rateLimitCall(c.ClientIP())
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            c.JSON(
                http.StatusTooManyRequests,
                gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"Rate Limit Hit"</span>})
        } <span class="hljs-keyword">else</span> {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"Your left over API request is"</span>: remainingLimit})
        }
    })
    r.GET(<span class="hljs-string">"/beta"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        c.JSON(
            http.StatusOK,
            gin.H{<span class="hljs-string">"message"</span>: <span class="hljs-string">"This is beta endpoint"</span>})
    })
    r.Run(<span class="hljs-string">":"</span> + os.Getenv(<span class="hljs-string">"PORT"</span>))
</code></pre>
<p>Then we create two <strong>GET</strong> endpoints, <code>/ping</code> and <code>/beta</code>. Every time someone hits the <code>/ping</code> endpoint we call the <code>rateLimitCall()</code> function. It checks and sets the rate limit of incoming requests from an <strong>IP address</strong>. All this is stored in the Redis instance we created.</p>
<p>So, now if the user has interacted with the <code>/ping</code> API endpoint for the first time, will create an entry with a limit of <strong>10 per hour</strong>. The limit number <strong>10</strong> comes from the <code>RATE_LIMIT</code> we set, and the hourly refresh form comes from the <code>redis_rate.PerHour(RATE_LIMIT)</code> function.</p>
<p>Next, we check if the user has a remaining limit. If yes, we will return a message with the number of requests they have remaining. Otherwise, if they hit the limit cap, we return a message letting them know this.</p>
<p>Apart from the <code>/ping</code> endpoint, we have another endpoint <code>/beta</code>. It returns a simple message, but later we’ll see how (using feature flags) we can completely turn on and off the functionality of this endpoint.</p>
<h3 id="heading-how-to-add-feature-flagging">How to Add Feature Flagging</h3>
<p>Now it’s time to add feature flagging capabilities to our app. We are going to use <a target="_blank" href="https://flagsmith.com/">Flagsmith</a>. Flagsmith is an open source software that lets us easily create and manage feature flags across web, mobile, and server-side applications.</p>
<p>Using Flagsmith, we can wrap features in a flag and then toggle them on or off for different environments, users, or user segments. And then you’ll be able to manage all of them from the Flagsmith dashboard without needing to redeploy.</p>
<p>So, let’s install the Flagsmith package by running the below command:</p>
<pre><code class="lang-bash">go get github.com/Flagsmith/flagsmith-go-client/v3
</code></pre>
<p>Then we import the package by giving it an alias <strong>flagsmith</strong>. Below is the updated functionality after we apply feature flagging to our existing code.</p>
<p>Let’s understand the changes we’ve made here (I’ll explain below the code snippet):</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"errors"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"os"</span>

    flagsmith <span class="hljs-string">"github.com/Flagsmith/flagsmith-go-client/v3"</span>
    <span class="hljs-string">"github.com/gin-gonic/gin"</span>
    <span class="hljs-string">"github.com/go-redis/redis_rate/v10"</span>
    <span class="hljs-string">"github.com/joho/godotenv"</span>
    <span class="hljs-string">"github.com/redis/go-redis/v9"</span>
)

<span class="hljs-keyword">var</span> (
    redisClient     *redis.Client
    limiter         *redis_rate.Limiter
    flagsmithClient *flagsmith.Client
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">initClients</span><span class="hljs-params">()</span></span> {
    redisClient = redis.NewClient(&amp;redis.Options{
        Addr: os.Getenv(<span class="hljs-string">"REDIS_URL"</span>),
    })
    limiter = redis_rate.NewLimiter(redisClient)
    flagsmithClient = flagsmith.NewClient(os.Getenv(<span class="hljs-string">"FLAGSMITH_ENVIRONMENT_KEY"</span>))
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    err := godotenv.Load()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Printf(<span class="hljs-string">"Loading environment variable from the host system"</span>)
    } <span class="hljs-keyword">else</span> {
        log.Printf(<span class="hljs-string">"Loading environment from .env file"</span>)
    }

    initClients()
    <span class="hljs-keyword">defer</span> redisClient.Close()

    r := gin.Default()
    r.GET(<span class="hljs-string">"/ping"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        err, remainingLimit := rateLimitCall(c.ClientIP())
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            c.JSON(
                http.StatusTooManyRequests,
                gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"Rate Limit Hit"</span>})
        } <span class="hljs-keyword">else</span> {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"Your left over API request is"</span>: remainingLimit})
        }
    })
    r.GET(<span class="hljs-string">"/beta"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        flags := getFeatureFlags()
        isEnabled, _ := flags.IsFeatureEnabled(<span class="hljs-string">"beta"</span>)
        <span class="hljs-keyword">if</span> isEnabled {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"message"</span>: <span class="hljs-string">"This is beta endpoint"</span>})
        } <span class="hljs-keyword">else</span> {
            c.String(http.StatusNotFound, <span class="hljs-string">"404 page not found"</span>)
        }
    })

    r.Run(<span class="hljs-string">":"</span> + os.Getenv(<span class="hljs-string">"PORT"</span>))
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">rateLimitCall</span><span class="hljs-params">(ClientIP <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(error, <span class="hljs-keyword">int</span>)</span></span> {

    ctx := context.Background()

    flags := getFeatureFlags()
    rateLimitInterface, _ := flags.GetFeatureValue(<span class="hljs-string">"rate_limit"</span>)
    RATE_LIMIT := <span class="hljs-keyword">int</span>(rateLimitInterface.(<span class="hljs-keyword">float64</span>))
    fmt.Println(<span class="hljs-string">"Current Rate Limit is"</span>, RATE_LIMIT)

    res, err := limiter.Allow(ctx, ClientIP, redis_rate.PerHour(RATE_LIMIT))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    <span class="hljs-keyword">if</span> res.Remaining == <span class="hljs-number">0</span> {
        <span class="hljs-keyword">return</span> errors.New(<span class="hljs-string">"You have hit the Rate Limit for the API. Try again later"</span>), <span class="hljs-number">0</span>
    }

    fmt.Println(<span class="hljs-string">"remaining request for"</span>, ClientIP, <span class="hljs-string">"is"</span>, res.Remaining)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, res.Remaining
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getFeatureFlags</span><span class="hljs-params">()</span> <span class="hljs-title">flagsmith</span>.<span class="hljs-title">Flags</span></span> {
    ctx := context.Background()
    flags, _ := flagsmithClient.GetEnvironmentFlags(ctx)
    <span class="hljs-keyword">return</span> flags
}
</code></pre>
<h3 id="heading-understanding-the-feature-flag-code-logic">Understanding the Feature Flag Code Logic</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getFeatureFlags</span><span class="hljs-params">()</span> <span class="hljs-title">flagsmith</span>.<span class="hljs-title">Flags</span></span> {
    ctx := context.Background()
    flags, _ := flagsmithClient.GetEnvironmentFlags(ctx)
    <span class="hljs-keyword">return</span> flags
}
</code></pre>
<p>First, let’s directly jump to the new <code>getFeatureFlags()</code> function we created at the bottom. This function will return all the flags we created on the Flagsmith dashboard, by calling the <code>GetEnvironmentFlags()</code> method on <code>flagsmithClient</code>.</p>
<p>We initiated the <code>flagsmithClient</code> inside the <code>initClients()</code> function. The Flagsmith Client needs the access key (the <code>NewClient()</code> function) that we can get from the Flagsmith dashboard. As we did for the Redis and Limter clients, we will store the client in a global variable for reusability. You’ll understand the dashboard, creating flags, and retrieving the key in later steps.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">rateLimitCall</span><span class="hljs-params">(ClientIP <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(error, <span class="hljs-keyword">int</span>)</span></span> {

    ctx := context.Background()

    flags := getFeatureFlags()
    rateLimitInterface, _ := flags.GetFeatureValue(<span class="hljs-string">"rate_limit"</span>)
    RATE_LIMIT := <span class="hljs-keyword">int</span>(rateLimitInterface.(<span class="hljs-keyword">float64</span>))
    fmt.Println(<span class="hljs-string">"Current Rate Limit is"</span>, RATE_LIMIT)

    res, err := limiter.Allow(ctx, ClientIP, redis_rate.PerHour(RATE_LIMIT))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    <span class="hljs-keyword">if</span> res.Remaining == <span class="hljs-number">0</span> {
        <span class="hljs-keyword">return</span> errors.New(<span class="hljs-string">"You have hit the Rate Limit for the API. Try again later"</span>), <span class="hljs-number">0</span>
    }

    fmt.Println(<span class="hljs-string">"remaining request for"</span>, ClientIP, <span class="hljs-string">"is"</span>, res.Remaining)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, res.Remaining
}
</code></pre>
<p>Now coming to the <code>rateLimitCall()</code> function, instead of getting <code>RATE_LIMIT</code> from the environment, we get the value from the <code>rate_limit</code> flag (that we will create later). We call <code>getFeatureFlags()</code> and get the flag <code>rate_limit</code> value out from all the flags.</p>
<p>By setting these as feature flags, we can dynamically change the limit anytime from the dashboard. We don’t need to change the code’s functionality or do it the traditional way by changing the <code>RATE_LIMIT</code> value and re-running the server so that it catches new updated values.</p>
<pre><code class="lang-go">    r.GET(<span class="hljs-string">"/beta"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        flags := getFeatureFlags()
        isEnabled, _ := flags.IsFeatureEnabled(<span class="hljs-string">"beta"</span>)
        <span class="hljs-keyword">if</span> isEnabled {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"message"</span>: <span class="hljs-string">"This is beta endpoint"</span>})
        } <span class="hljs-keyword">else</span> {
            c.String(http.StatusNotFound, <span class="hljs-string">"404 page not found"</span>)
        }
    })
</code></pre>
<p>Now coming to the <code>/beta</code> endpoint, based on whether the beta flag is enabled or disabled, this endpoint will serve the query. Otherwise, it will act as a non-reachable endpoint and return a 404 error message.</p>
<p>In our example, I have added a basic placeholder message to show how it will work, but this opens new possibilities in testing and initial releases (beta). If the API has a new endpoint, we can wrap the functionality in the feature flag and make it available and unavailable with a single click of a button. Also, we can do a lot more like scheduling and canary releases.</p>
<p>Also, our <code>.env</code> file will look like this. We have removed <code>RATE_LIMIT</code> and added <code>FLAGSMITH_ENVIRONMENT_KEY</code>.</p>
<pre><code class="lang-bash">REDIS_URL=localhost:6379
PORT=8080
FLAGSMITH_ENVIRONMENT_KEY=ser.ZRd***********469
</code></pre>
<h3 id="heading-how-to-create-feature-flags-in-the-flasgsmith-dashboard">How to Create Feature Flags in the Flasgsmith Dashboard</h3>
<p>Let’s head to the Flagsmith dashboard to create the flags we used above and get the access key. If you don’t have a Flagsmith account you can sign up for free <a target="_blank" href="https://app.flagsmith.com/signup">here</a>.</p>
<p>After you sign up you will be prompted to create an organisation and a project. Project separation is good, as it helps us isolate logic for different projects. Once you are done, you will see a dashboard, just like the screenshot below.</p>
<p>We have loads of functionalities from integrations to scheduling the flags to compare the changes. Apart from Go, Flagsmith provides many <a target="_blank" href="https://docs.flagsmith.com/clients/">SDKs</a>. You can click on where the language name is written and it will give you some boilerplate code for that language.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544211942/57f3651f-b62a-4b8f-beb7-4320ef0e0a8e.png" alt="Screenshot of a web interface labeled &quot;Features&quot; for managing feature flags and remote config. It includes examples of Go code for installing the SDK and initializing a project, with options to test API values. There are buttons and tabs for navigation and settings." class="image--center mx-auto" width="2985" height="1887" loading="lazy"></p>
<h3 id="heading-rate-limiting-feature-flag">Rate Limiting Feature Flag</h3>
<p>Now, let's create our first feature flag for the rate limit. Click on the <strong>Create Feature</strong> button in the top right corner. A sidebar window will open up. Set the name, then to make the flag turn on the right way while creating, we can select <strong>Enabled by default.</strong></p>
<p>In the value section, we need to set the flag value. It can take formats like Txt, JSON, XML, and so on. As our feature value is simple text like 20, 30, and so on, we will choose Txt (the default one) and set a random limit – we’ll go with <strong>20</strong>.</p>
<p>You can also give tags and descriptions. Tags can be helpful when filtering out the Feature Flags. For example, we can create a tag <code>backend</code> to filter out all the feature flags related to Backend. The description is a concise explanation of what this particular future flag does when it is enabled (and will help with future understanding).</p>
<p>The screenshot below shows how it will look after filling in the details. Then, click on the <strong>Create Feature</strong> button to create the flag.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544238847/4e5cf3ab-1fb6-4783-afcc-39adcebae48e.png" alt="A screenshot of a web application interface showing the creation of a new feature. On the left, there is a menu with options like Features and SDK Keys. On the right, fields for adding a new feature are visible, including an ID/Name, a toggle for enabling by default, a value set to 20, and options for tags and descriptions. There is a note indicating feature creation for all environments, with a &quot;Create Feature&quot; button at the bottom." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<h3 id="heading-beta-feature-flag">Beta Feature Flag</h3>
<p>Let’s now create a second, <code>beta</code> feature flag. It will be the same process as the first one, but in this one, we don’t need to set any flag value and leave that column empty. Once we create both flags, our dashboard will look like this. It shows the flag name, value, current state (view), and so on.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544256561/3735429f-8dd0-4f0f-a01e-b4a4a7b5aa75.png" alt="A software interface showcasing a &quot;Features&quot; section with toggles for &quot;beta&quot; and &quot;rate_limit&quot; features. The page includes navigation options on the left and buttons for creating features and running tests." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<h3 id="heading-getting-the-access-key">Getting the Access Key</h3>
<p>To get the Access Key, click on the <strong>SDK Keys</strong> from the sidebar, and click the <strong>Create Server-side Environment Key</strong> button to generate a key. As our app is server-side, it’s good to use that one only. Then copy and paste that key into the value placed in <code>.env</code> for the <code>FLAGSMITH_ENVIRONMENT_KEY</code> key.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544280780/fc37cb29-3069-4e2f-b35b-eea7632c47cd.png" alt="Screenshot of a software interface showing &quot;Client-side Environment Key&quot; and &quot;Server-side Environment Keys&quot; sections. A button labeled &quot;Create Server-side Environment Key&quot; is displayed prominently. The sidebar menu includes options like &quot;SDK Keys&quot; and &quot;Environment Settings.&quot;" class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<h3 id="heading-running-the-api">Running the API</h3>
<p>Now everything is set, so let’s head over back to IDE and run the server by executing the <code>go run main.go</code> command in the terminal. We will see this message In the terminal. In case you encounter any errors, just check that the packages are correctly installed, the variables are correctly set, and the app accesses the Redis instance.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544780659/95fbbb17-43c3-4cd1-b84f-020c08ec38d3.png" alt="Screenshot of a VS Code window showing a Go project with the file &quot;main.go&quot; open. The code includes functions for rate limiting API calls and retrieving feature flags. The terminal at the bottom displays the output of running the application, with warnings and status messages related to a web server." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<p>Now if we visit <a target="_blank" href="http://localhost:8080/ping"><strong>localhost:8080/ping</strong></a>, we will get a message <code>{"Your left over API request is":19}</code>. The limit was 20, we did one request now, and the remaining is 19.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544374092/bc97064c-285e-44fa-990d-51a52a671d26.png" alt="A browser window displaying a webpage at &quot;localhost:8080/ping&quot; showing the JSON message: {&quot;Your left over API request is&quot;: 19}." class="image--center mx-auto" width="2324" height="780" loading="lazy"></p>
<h3 id="heading-updating-the-ratelimit-flag">Updating the <code>rate_limit</code> Flag</h3>
<p>Let’s update the <code>rate_limit</code> flag value to 10 and see what happens. To do so, again visit the Flagsmith dashboard and click on the flag name. A side menu bar will open. Update the value to 10, and click on the <strong>Update Feature Value</strong> button.</p>
<p>We can also schedule the update. For example, this can be useful when we expect a spike in traffic at a certain timeframe and reduce the limit per user to reduce server load.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730545050685/f253ea86-de3d-4a6a-b5fd-35f489da86cf.png" alt="Screenshot of a software dashboard showing a feature management interface. The &quot;rate_limit&quot; feature is enabled with a value of 10. Options include editing value, segment overrides, and scheduling updates." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<p>If you now visit <a target="_blank" href="http://localhost:8080/ping"><strong>localhost:8080/ping</strong></a>, you will get a message <code>{"Your left over API request is":8}</code> – because the total limit is 10 and we have already requested two times.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544415108/97e6fd9c-b5a1-4143-877a-6724cf871a6b.png" alt="Browser window displaying a JSON response with the text: &quot;Your left over API request is: 8&quot;." class="image--center mx-auto" width="2324" height="780" loading="lazy"></p>
<p>Let's now test the <code>/beta</code> endpoint. Visit <a target="_blank" href="http://localhost:8080/beta">localhost:8080/beta</a>, and we will see a message <code>{"message":"This is beta endpoint"}</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544479869/623d8b72-3648-46ae-b79f-409da44c1d38.png" alt="Screenshot of a web browser displaying JSON data at the URL &quot;localhost:8080/beta&quot; with the message: &quot;This is beta endpoint&quot;." class="image--center mx-auto" width="2324" height="780" loading="lazy"></p>
<p>Now go back to the Flagsmith dashboard and toggle the switch to disable this flag. Now visit the the URL. You will get a 404 message like this endpoint never existed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544488522/518c58b2-f767-4396-9020-99e8cd01586a.png" alt="Screenshot of a browser window displaying a &quot;404 page not found&quot; error message." class="image--center mx-auto" width="2324" height="780" loading="lazy"></p>
<p>Now that we’ve set up the functionality and demoed the feature flagging capabilities, let’s see how we can integrate the Flasgsmith GitHub App.</p>
<h3 id="heading-how-to-integrate-feature-flags-with-the-github-app">How to Integrate Feature Flags with the GitHub App</h3>
<p>First, make sure you have pushed your app to GitHub. After that, install the GitHub Flasgsmith App on your repo from the <a target="_blank" href="https://github.com/apps/flagsmith">GitHub Marketplace</a>.</p>
<p>By integrating GitHub and Falagsmith, we can view updates on your feature flags/features as comments in GitHub Issues and Pull Requests. This allows us to easily track features, from creating an issue to merging a PR and deploying the changes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544845464/dcce9af3-a34f-420a-b9c4-2968d47fda70.png" alt="Screenshot of the Flagsmith GitHub app integration page, detailing its features and benefits, with an option to install the app." class="image--center mx-auto" width="2997" height="1885" loading="lazy"></p>
<p>Then select your organisation and the repositories where you want to install the app. You can install it on all of your repos or select a particular one.</p>
<p>As you install it, you will be auto-redirected to the Flagmsith dashboard to configure and complete the integration. Most of the data will be pre-populated, so you just need to select and add a project, and then save the configuration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544640407/8ac36f96-d61b-47f7-ad3a-f914f0f01824.png" alt="Screenshot of a webpage for configuring GitHub integration with Flagsmith. It includes fields for selecting the organization, project, and repository, with options set for &quot;Pradumna,&quot; &quot;go-api,&quot; and &quot;go-redis-flagsmith.&quot; There is an &quot;Add Project&quot; button and a &quot;Save Configuration&quot; button at the bottom." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<p>Once you hit the Save <strong>Configuration Button</strong>, it will redirect you back to the main Flagsmith dashboard where we were previously working.</p>
<p>Now let’s link one of the existing flags with the GitHub issue/pull request (raise a dummy PR/issue to test it), or you can create a new flag to test. Let’s proceed with the beta flag which we already created for the <code>beta</code> endpoint.</p>
<p>To link the flag with an existing issue or a pull request, click on the flag name, and a side menu will pop up from the right. Then, choose the 'Link' tab. Then select the Pull Request option, and choose the Pull Request you want to link. All of your Issues and Pull Requests linked to this flag are visible below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730546404697/0fb1c515-ab42-494d-ac84-87e076d30607.png" alt="A screenshot of a development environment interface showing the &quot;Features&quot; section, with a sidebar menu on the left. The &quot;Edit Feature: beta&quot; panel is open on the right, displaying options to link an issue or pull request and a listed pull request titled &quot;feat: Update the beta endpoint feature (#2)&quot; with its status marked as open." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<p>To verify that the flag is successfully linked, click the hyperlink with the arrow icon below the <strong>Name</strong> column heading. It will navigate you to that particular Issue/Pull Request on GitHub. You can see that the Flagsmith GitHub App has commented below with all the details, such as environment, enabled value, and so on.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730545132237/4601a08e-62d9-4ebc-890e-89430bf6624e.png" alt="GitHub pull request page showing a request titled &quot;feat: Update the beta endpoint feature #2&quot; to merge a commit from the &quot;beta&quot; branch into &quot;main&quot;. It includes a user comment about the update and a Flagsmith bot comment showing feature status for production and development environments. The pull request is open, with no reviews yet." class="image--center mx-auto" width="2990" height="1614" loading="lazy"></p>
<h3 id="heading-testing-the-flagsmith-github-app">Testing the Flagsmith GitHub App</h3>
<p>After this, when you make any changes to the flag settings, such as turning on/off the flag or changing the value, the bot will comment with all the updated details.</p>
<p>Let’s test by turning the flag off. As soon as you turn off the flash from the Dashboard, the bot should comment that the flag has now been disabled:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730545146615/b76f2f21-369a-4617-b55b-abc3201a1c52.png" alt="Image showing a GitHub pull request interface. The pull request is titled &quot;feat: Update the beta endpoint feature #2&quot; and shows an update from the flagsmith bot indicating that the &quot;beta&quot; feature for the &quot;Development&quot; environment is currently disabled." class="image--center mx-auto" width="2434" height="688" loading="lazy"></p>
<p>That’s it. That is how it’s simple to integrate Flagsmith with GitHub.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>To sum it up, you now know how you can leverage feature flags as a backend developer to change the functionality of your app on the fly.</p>
<p>To take things to the next level, we integrated our demo app with the Flagsmith GitHub app so it could stay updated with the changes to our feature flags’ status on Pull Requests/Issues without having to manually update them.</p>
<p>Check out the Flagsmith <a target="_blank" href="https://github.com/Flagsmith/flagsmith">repo here</a> and don't forget to give each of these projects a star to show your support. You can also join their amazing <a target="_blank" href="https://discord.com/invite/hFhxNtXzgm">community</a> to get technical support.</p>
<p>You can connect with me - Pradumna Saraf, on socials <a target="_blank" href="https://links.pradumnasaraf.dev/">here</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
