Skip to content

Shelf Framework Guide

Framework: Shelf 1.x Language: Dart 3.x Type: Middleware-Based HTTP Server Use Cases: REST APIs, Microservices, Backend Services


Overview

Shelf is a web server middleware framework for Dart. It provides a simple, composable model for building HTTP servers using middleware pipelines. Shelf is the foundation for many Dart backend frameworks.

Key Features

  • Middleware-based architecture
  • Composable request/response handlers
  • Streaming support
  • WebSocket support (via shelf_web_socket)
  • Simple and lightweight

Project Structure

myapp/
├── bin/
│   └── server.dart               # Entry point
├── lib/
│   ├── src/
│   │   ├── app.dart              # Application setup
│   │   ├── config/
│   │   │   └── config.dart       # Configuration
│   │   ├── handlers/
│   │   │   ├── users_handler.dart
│   │   │   └── health_handler.dart
│   │   ├── middleware/
│   │   │   ├── auth_middleware.dart
│   │   │   ├── cors_middleware.dart
│   │   │   └── logging_middleware.dart
│   │   ├── models/
│   │   │   └── user.dart
│   │   ├── repositories/
│   │   │   └── user_repository.dart
│   │   └── services/
│   │       └── user_service.dart
│   └── myapp.dart                # Library export
├── test/
│   └── handlers/
│       └── users_handler_test.dart
├── pubspec.yaml
├── analysis_options.yaml
└── Dockerfile

Application Setup

Entry Point

// bin/server.dart
import 'dart:io';

import 'package:shelf/shelf_io.dart' as shelf_io;

import 'package:myapp/myapp.dart';

Future<void> main() async {
  final config = Config.fromEnvironment();
  final app = Application(config);

  final handler = await app.createHandler();

  final server = await shelf_io.serve(
    handler,
    InternetAddress.anyIPv4,
    config.port,
  );

  print('Server running on http://${server.address.host}:${server.port}');

  // Graceful shutdown
  ProcessSignal.sigint.watch().listen((_) async {
    print('Shutting down...');
    await app.close();
    await server.close();
    exit(0);
  });
}

Application Class

// lib/src/app.dart
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

import 'config/config.dart';
import 'handlers/health_handler.dart';
import 'handlers/users_handler.dart';
import 'middleware/auth_middleware.dart';
import 'middleware/cors_middleware.dart';
import 'middleware/logging_middleware.dart';
import 'repositories/user_repository.dart';
import 'services/user_service.dart';

class Application {
  final Config config;

  late final UserRepository _userRepository;
  late final UserService _userService;

  Application(this.config);

  Future<Handler> createHandler() async {
    // Initialize dependencies
    _userRepository = UserRepository();
    _userService = UserService(_userRepository);

    // Create handlers
    final healthHandler = HealthHandler();
    final usersHandler = UsersHandler(_userService);

    // Build router
    final router = Router()
      ..mount('/health', healthHandler.router.call)
      ..mount('/api/v1/users', usersHandler.router.call);

    // Build middleware pipeline
    final pipeline = const Pipeline()
        .addMiddleware(loggingMiddleware())
        .addMiddleware(corsMiddleware())
        .addMiddleware(handleErrors())
        .addHandler(router.call);

    return pipeline;
  }

  Future<void> close() async {
    await _userRepository.close();
  }
}

// Error handling middleware
Middleware handleErrors() {
  return (Handler innerHandler) {
    return (Request request) async {
      try {
        return await innerHandler(request);
      } on NotFoundException catch (e) {
        return Response.notFound(
          jsonEncode({'error': e.message}),
          headers: {'Content-Type': 'application/json'},
        );
      } on ValidationException catch (e) {
        return Response(
          422,
          body: jsonEncode({'error': e.message, 'errors': e.errors}),
          headers: {'Content-Type': 'application/json'},
        );
      } on UnauthorizedException {
        return Response.forbidden(
          jsonEncode({'error': 'Unauthorized'}),
          headers: {'Content-Type': 'application/json'},
        );
      } catch (e, stack) {
        print('Error: $e\n$stack');
        return Response.internalServerError(
          body: jsonEncode({'error': 'Internal server error'}),
          headers: {'Content-Type': 'application/json'},
        );
      }
    };
  };
}

Configuration

// lib/src/config/config.dart
import 'dart:io';

class Config {
  final int port;
  final String databaseUrl;
  final String jwtSecret;
  final bool isDevelopment;

  const Config({
    required this.port,
    required this.databaseUrl,
    required this.jwtSecret,
    required this.isDevelopment,
  });

  factory Config.fromEnvironment() {
    return Config(
      port: int.parse(Platform.environment['PORT'] ?? '8080'),
      databaseUrl: Platform.environment['DATABASE_URL'] ??
          'postgres://localhost/myapp',
      jwtSecret: Platform.environment['JWT_SECRET'] ??
          'development-secret-key',
      isDevelopment: Platform.environment['DART_ENV'] != 'production',
    );
  }
}

Handlers (Controllers)

Basic Handler

// lib/src/handlers/health_handler.dart
import 'dart:convert';

import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

class HealthHandler {
  Router get router {
    final router = Router();

    router.get('/', _health);
    router.get('/ready', _ready);
    router.get('/live', _live);

    return router;
  }

  Response _health(Request request) {
    return Response.ok(
      jsonEncode({
        'status': 'healthy',
        'timestamp': DateTime.now().toIso8601String(),
      }),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Response _ready(Request request) {
    // Check database connection, etc.
    return Response.ok(
      jsonEncode({'ready': true}),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Response _live(Request request) {
    return Response.ok(
      jsonEncode({'live': true}),
      headers: {'Content-Type': 'application/json'},
    );
  }
}

CRUD Handler

// lib/src/handlers/users_handler.dart
import 'dart:convert';

import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

import '../models/user.dart';
import '../services/user_service.dart';
import '../middleware/auth_middleware.dart';

class UsersHandler {
  final UserService _userService;

  UsersHandler(this._userService);

  Router get router {
    final router = Router();

    // Public routes
    router.post('/register', _register);
    router.post('/login', _login);

    // Protected routes (add auth middleware individually or to group)
    router.get('/', _authMiddleware(_getAll));
    router.get('/<id>', _authMiddleware(_getById));
    router.put('/<id>', _authMiddleware(_update));
    router.delete('/<id>', _authMiddleware(_delete));

    return router;
  }

  Handler _authMiddleware(Handler handler) {
    return const Pipeline()
        .addMiddleware(authMiddleware())
        .addHandler(handler);
  }

  Future<Response> _register(Request request) async {
    final body = await request.readAsString();
    final json = jsonDecode(body) as Map<String, dynamic>;

    final user = await _userService.register(
      email: json['email'] as String,
      password: json['password'] as String,
      name: json['name'] as String,
    );

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

  Future<Response> _login(Request request) async {
    final body = await request.readAsString();
    final json = jsonDecode(body) as Map<String, dynamic>;

    final result = await _userService.login(
      email: json['email'] as String,
      password: json['password'] as String,
    );

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

  Future<Response> _getAll(Request request) async {
    final page = int.tryParse(request.url.queryParameters['page'] ?? '1') ?? 1;
    final limit = int.tryParse(request.url.queryParameters['limit'] ?? '20') ?? 20;

    final users = await _userService.getAll(page: page, limit: limit);

    return Response.ok(
      jsonEncode({
        'data': users.map((u) => u.toJson()).toList(),
        'page': page,
        'limit': limit,
      }),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future<Response> _getById(Request request, String id) async {
    final user = await _userService.getById(id);

    if (user == null) {
      throw NotFoundException('User not found');
    }

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

  Future<Response> _update(Request request, String id) async {
    final currentUser = request.context['user'] as User;

    // Check authorization
    if (currentUser.id != id && !currentUser.isAdmin) {
      throw UnauthorizedException();
    }

    final body = await request.readAsString();
    final json = jsonDecode(body) as Map<String, dynamic>;

    final user = await _userService.update(id, json);

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

  Future<Response> _delete(Request request, String id) async {
    final currentUser = request.context['user'] as User;

    if (currentUser.id != id && !currentUser.isAdmin) {
      throw UnauthorizedException();
    }

    await _userService.delete(id);

    return Response(204);
  }
}

Middleware

Logging Middleware

// lib/src/middleware/logging_middleware.dart
import 'package:shelf/shelf.dart';

Middleware loggingMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      final stopwatch = Stopwatch()..start();

      print('[${DateTime.now()}] ${request.method} ${request.requestedUri}');

      final response = await innerHandler(request);

      stopwatch.stop();
      print(
        '[${DateTime.now()}] ${request.method} ${request.requestedUri} '
        '${response.statusCode} ${stopwatch.elapsedMilliseconds}ms',
      );

      return response;
    };
  };
}

CORS Middleware

// lib/src/middleware/cors_middleware.dart
import 'package:shelf/shelf.dart';

Middleware corsMiddleware({
  List<String> allowedOrigins = const ['*'],
  List<String> allowedMethods = const ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  List<String> allowedHeaders = const ['Content-Type', 'Authorization'],
}) {
  return (Handler innerHandler) {
    return (Request request) async {
      // Handle preflight requests
      if (request.method == 'OPTIONS') {
        return Response.ok(
          '',
          headers: _corsHeaders(allowedOrigins, allowedMethods, allowedHeaders),
        );
      }

      final response = await innerHandler(request);

      return response.change(
        headers: {
          ...response.headers,
          ..._corsHeaders(allowedOrigins, allowedMethods, allowedHeaders),
        },
      );
    };
  };
}

Map<String, String> _corsHeaders(
  List<String> origins,
  List<String> methods,
  List<String> headers,
) {
  return {
    'Access-Control-Allow-Origin': origins.join(', '),
    'Access-Control-Allow-Methods': methods.join(', '),
    'Access-Control-Allow-Headers': headers.join(', '),
    'Access-Control-Max-Age': '86400',
  };
}

Authentication Middleware

// lib/src/middleware/auth_middleware.dart
import 'dart:convert';

import 'package:shelf/shelf.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';

import '../models/user.dart';
import '../config/config.dart';

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

      if (authHeader == null || !authHeader.startsWith('Bearer ')) {
        throw UnauthorizedException();
      }

      final token = authHeader.substring(7);

      try {
        final config = request.context['config'] as Config;
        final jwt = JWT.verify(token, SecretKey(config.jwtSecret));

        final payload = jwt.payload as Map<String, dynamic>;
        final user = User.fromJson(payload['user'] as Map<String, dynamic>);

        // Add user to request context
        final updatedRequest = request.change(
          context: {...request.context, 'user': user},
        );

        return await innerHandler(updatedRequest);
      } on JWTExpiredException {
        throw UnauthorizedException('Token expired');
      } on JWTException {
        throw UnauthorizedException('Invalid token');
      }
    };
  };
}

// Optional auth - doesn't fail if no token
Middleware optionalAuthMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      final authHeader = request.headers['Authorization'];

      if (authHeader != null && authHeader.startsWith('Bearer ')) {
        final token = authHeader.substring(7);

        try {
          final config = request.context['config'] as Config;
          final jwt = JWT.verify(token, SecretKey(config.jwtSecret));

          final payload = jwt.payload as Map<String, dynamic>;
          final user = User.fromJson(payload['user'] as Map<String, dynamic>);

          return await innerHandler(
            request.change(context: {...request.context, 'user': user}),
          );
        } catch (_) {
          // Ignore invalid tokens
        }
      }

      return await innerHandler(request);
    };
  };
}

// Exceptions
class UnauthorizedException implements Exception {
  final String message;
  UnauthorizedException([this.message = 'Unauthorized']);
}

class NotFoundException implements Exception {
  final String message;
  NotFoundException(this.message);
}

class ValidationException implements Exception {
  final String message;
  final Map<String, List<String>> errors;
  ValidationException(this.message, [this.errors = const {}]);
}

Rate Limiting Middleware

// lib/src/middleware/rate_limit_middleware.dart
import 'dart:async';

import 'package:shelf/shelf.dart';

class RateLimiter {
  final int maxRequests;
  final Duration window;
  final Map<String, List<DateTime>> _requests = {};

  RateLimiter({
    this.maxRequests = 100,
    this.window = const Duration(minutes: 1),
  });

  bool isAllowed(String key) {
    final now = DateTime.now();
    final windowStart = now.subtract(window);

    _requests[key] = (_requests[key] ?? [])
        .where((time) => time.isAfter(windowStart))
        .toList();

    if (_requests[key]!.length >= maxRequests) {
      return false;
    }

    _requests[key]!.add(now);
    return true;
  }

  int remaining(String key) {
    final now = DateTime.now();
    final windowStart = now.subtract(window);

    final count = (_requests[key] ?? [])
        .where((time) => time.isAfter(windowStart))
        .length;

    return (maxRequests - count).clamp(0, maxRequests);
  }
}

Middleware rateLimitMiddleware({
  int maxRequests = 100,
  Duration window = const Duration(minutes: 1),
}) {
  final limiter = RateLimiter(maxRequests: maxRequests, window: window);

  return (Handler innerHandler) {
    return (Request request) async {
      final clientIp = request.headers['X-Forwarded-For'] ??
          request.headers['X-Real-IP'] ??
          'unknown';

      if (!limiter.isAllowed(clientIp)) {
        return Response(
          429,
          body: jsonEncode({'error': 'Too many requests'}),
          headers: {
            'Content-Type': 'application/json',
            'Retry-After': '60',
            'X-RateLimit-Remaining': '0',
          },
        );
      }

      final response = await innerHandler(request);

      return response.change(
        headers: {
          ...response.headers,
          'X-RateLimit-Remaining': '${limiter.remaining(clientIp)}',
          'X-RateLimit-Limit': '$maxRequests',
        },
      );
    };
  };
}

Models

User Model

// lib/src/models/user.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const User._();

  const factory User({
    required String id,
    required String email,
    required String name,
    @Default(false) bool isAdmin,
    @JsonKey(includeToJson: false) String? passwordHash,
    required DateTime createdAt,
    DateTime? updatedAt,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  Map<String, dynamic> toPublicJson() {
    return {
      'id': id,
      'email': email,
      'name': name,
      'isAdmin': isAdmin,
      'createdAt': createdAt.toIso8601String(),
      if (updatedAt != null) 'updatedAt': updatedAt!.toIso8601String(),
    };
  }
}

@freezed
class LoginResult with _$LoginResult {
  const factory LoginResult({
    required String token,
    required User user,
  }) = _LoginResult;
}

Services

User Service

// lib/src/services/user_service.dart
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:bcrypt/bcrypt.dart';

import '../config/config.dart';
import '../models/user.dart';
import '../repositories/user_repository.dart';
import '../middleware/auth_middleware.dart';

class UserService {
  final UserRepository _repository;
  final Config _config;

  UserService(this._repository, this._config);

  Future<User> register({
    required String email,
    required String password,
    required String name,
  }) async {
    // Validate
    _validateEmail(email);
    _validatePassword(password);

    // Check if email exists
    final existing = await _repository.findByEmail(email);
    if (existing != null) {
      throw ValidationException(
        'Email already exists',
        {'email': ['Email is already registered']},
      );
    }

    // Hash password
    final passwordHash = BCrypt.hashpw(password, BCrypt.gensalt());

    // Create user
    final user = await _repository.create(
      email: email.toLowerCase().trim(),
      passwordHash: passwordHash,
      name: name.trim(),
    );

    return user;
  }

  Future<LoginResult> login({
    required String email,
    required String password,
  }) async {
    final user = await _repository.findByEmail(email.toLowerCase().trim());

    if (user == null || user.passwordHash == null) {
      throw UnauthorizedException('Invalid credentials');
    }

    final isValid = BCrypt.checkpw(password, user.passwordHash!);
    if (!isValid) {
      throw UnauthorizedException('Invalid credentials');
    }

    // Generate JWT
    final jwt = JWT(
      {
        'user': user.toPublicJson(),
        'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
      },
      issuer: 'myapp',
      subject: user.id,
    );

    final token = jwt.sign(
      SecretKey(_config.jwtSecret),
      expiresIn: const Duration(days: 7),
    );

    return LoginResult(token: token, user: user);
  }

  Future<List<User>> getAll({int page = 1, int limit = 20}) async {
    return _repository.findAll(
      offset: (page - 1) * limit,
      limit: limit,
    );
  }

  Future<User?> getById(String id) async {
    return _repository.findById(id);
  }

  Future<User> update(String id, Map<String, dynamic> data) async {
    final user = await _repository.findById(id);
    if (user == null) {
      throw NotFoundException('User not found');
    }

    return _repository.update(
      id,
      name: data['name'] as String?,
      email: data['email'] as String?,
    );
  }

  Future<void> delete(String id) async {
    final user = await _repository.findById(id);
    if (user == null) {
      throw NotFoundException('User not found');
    }

    await _repository.delete(id);
  }

  void _validateEmail(String email) {
    final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
    if (!emailRegex.hasMatch(email)) {
      throw ValidationException(
        'Invalid email',
        {'email': ['Please enter a valid email address']},
      );
    }
  }

  void _validatePassword(String password) {
    if (password.length < 8) {
      throw ValidationException(
        'Invalid password',
        {'password': ['Password must be at least 8 characters']},
      );
    }
  }
}

Repositories

User Repository

// lib/src/repositories/user_repository.dart
import '../models/user.dart';

abstract class UserRepository {
  Future<User> create({
    required String email,
    required String passwordHash,
    required String name,
  });

  Future<User?> findById(String id);
  Future<User?> findByEmail(String email);
  Future<List<User>> findAll({int offset = 0, int limit = 20});
  Future<User> update(String id, {String? name, String? email});
  Future<void> delete(String id);
  Future<void> close();
}

// In-memory implementation for example
class InMemoryUserRepository implements UserRepository {
  final Map<String, User> _users = {};
  int _idCounter = 0;

  @override
  Future<User> create({
    required String email,
    required String passwordHash,
    required String name,
  }) async {
    final id = (++_idCounter).toString();
    final user = User(
      id: id,
      email: email,
      name: name,
      passwordHash: passwordHash,
      createdAt: DateTime.now(),
    );
    _users[id] = user;
    return user;
  }

  @override
  Future<User?> findById(String id) async {
    return _users[id];
  }

  @override
  Future<User?> findByEmail(String email) async {
    return _users.values.where((u) => u.email == email).firstOrNull;
  }

  @override
  Future<List<User>> findAll({int offset = 0, int limit = 20}) async {
    return _users.values.skip(offset).take(limit).toList();
  }

  @override
  Future<User> update(String id, {String? name, String? email}) async {
    final user = _users[id];
    if (user == null) throw Exception('User not found');

    final updated = user.copyWith(
      name: name ?? user.name,
      email: email ?? user.email,
      updatedAt: DateTime.now(),
    );
    _users[id] = updated;
    return updated;
  }

  @override
  Future<void> delete(String id) async {
    _users.remove(id);
  }

  @override
  Future<void> close() async {}
}

WebSocket Support

// lib/src/handlers/websocket_handler.dart
import 'dart:convert';

import 'package:shelf/shelf.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

class WebSocketHandler {
  final Set<WebSocketChannel> _clients = {};

  Handler get handler => webSocketHandler(_onConnection);

  void _onConnection(WebSocketChannel webSocket) {
    _clients.add(webSocket);
    print('Client connected. Total: ${_clients.length}');

    webSocket.stream.listen(
      (message) => _handleMessage(webSocket, message),
      onDone: () {
        _clients.remove(webSocket);
        print('Client disconnected. Total: ${_clients.length}');
      },
      onError: (error) {
        print('WebSocket error: $error');
        _clients.remove(webSocket);
      },
    );
  }

  void _handleMessage(WebSocketChannel sender, dynamic message) {
    try {
      final data = jsonDecode(message as String) as Map<String, dynamic>;
      final type = data['type'] as String;

      switch (type) {
        case 'ping':
          sender.sink.add(jsonEncode({'type': 'pong'}));
          break;
        case 'broadcast':
          _broadcast(data['payload']);
          break;
        default:
          sender.sink.add(jsonEncode({
            'type': 'error',
            'message': 'Unknown message type',
          }));
      }
    } catch (e) {
      sender.sink.add(jsonEncode({
        'type': 'error',
        'message': 'Invalid message format',
      }));
    }
  }

  void _broadcast(dynamic payload) {
    final message = jsonEncode({
      'type': 'message',
      'payload': payload,
    });

    for (final client in _clients) {
      client.sink.add(message);
    }
  }
}

Testing

Handler Tests

// test/handlers/users_handler_test.dart
import 'dart:convert';

import 'package:shelf/shelf.dart';
import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';

import 'package:myapp/src/handlers/users_handler.dart';
import 'package:myapp/src/services/user_service.dart';
import 'package:myapp/src/models/user.dart';

class MockUserService extends Mock implements UserService {}

void main() {
  late MockUserService mockService;
  late UsersHandler handler;

  setUp(() {
    mockService = MockUserService();
    handler = UsersHandler(mockService);
  });

  group('UsersHandler', () {
    group('POST /register', () {
      test('returns 201 with created user', () async {
        final user = User(
          id: '1',
          email: 'test@example.com',
          name: 'Test User',
          createdAt: DateTime.now(),
        );

        when(() => mockService.register(
              email: any(named: 'email'),
              password: any(named: 'password'),
              name: any(named: 'name'),
            )).thenAnswer((_) async => user);

        final request = Request(
          'POST',
          Uri.parse('http://localhost/register'),
          body: jsonEncode({
            'email': 'test@example.com',
            'password': 'password123',
            'name': 'Test User',
          }),
        );

        final response = await handler.router.call(request);

        expect(response.statusCode, equals(201));

        final body = jsonDecode(await response.readAsString());
        expect(body['email'], equals('test@example.com'));
      });

      test('returns 422 with validation errors', () async {
        when(() => mockService.register(
              email: any(named: 'email'),
              password: any(named: 'password'),
              name: any(named: 'name'),
            )).thenThrow(ValidationException(
          'Invalid email',
          {'email': ['Please enter a valid email']},
        ));

        final request = Request(
          'POST',
          Uri.parse('http://localhost/register'),
          body: jsonEncode({
            'email': 'invalid',
            'password': 'password123',
            'name': 'Test',
          }),
        );

        // Need to wrap with error handler
        final handlerWithErrors = const Pipeline()
            .addMiddleware(handleErrors())
            .addHandler(handler.router.call);

        final response = await handlerWithErrors(request);

        expect(response.statusCode, equals(422));
      });
    });

    group('GET /', () {
      test('returns paginated users', () async {
        final users = [
          User(id: '1', email: 'a@test.com', name: 'A', createdAt: DateTime.now()),
          User(id: '2', email: 'b@test.com', name: 'B', createdAt: DateTime.now()),
        ];

        when(() => mockService.getAll(page: any(named: 'page'), limit: any(named: 'limit')))
            .thenAnswer((_) async => users);

        // Add authenticated user to context
        final request = Request(
          'GET',
          Uri.parse('http://localhost/?page=1&limit=20'),
        ).change(context: {
          'user': User(id: '1', email: 'admin@test.com', name: 'Admin', createdAt: DateTime.now()),
        });

        final response = await handler.router.call(request);

        expect(response.statusCode, equals(200));

        final body = jsonDecode(await response.readAsString());
        expect(body['data'], hasLength(2));
      });
    });
  });
}

Configuration

pubspec.yaml

name: myapp
description: Shelf backend application
version: 1.0.0

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  shelf: ^1.4.0
  shelf_router: ^1.1.0
  shelf_web_socket: ^1.0.0

  # Data
  freezed_annotation: ^2.4.0
  json_annotation: ^4.8.0

  # Auth
  dart_jsonwebtoken: ^2.12.0
  bcrypt: ^1.1.0

  # Utils
  args: ^2.4.0

dev_dependencies:
  test: ^1.24.0
  mocktail: ^1.0.0

  # Code generation
  build_runner: ^2.4.0
  freezed: ^2.4.0
  json_serializable: ^6.7.0

executables:
  server: server

Dockerfile

# Build stage
FROM dart:stable AS build

WORKDIR /app

# Copy dependencies
COPY pubspec.* ./
RUN dart pub get

# Copy source
COPY . .

# Generate code
RUN dart run build_runner build --delete-conflicting-outputs

# Build
RUN dart compile exe bin/server.dart -o bin/server

# Runtime stage
FROM scratch

COPY --from=build /runtime/ /
COPY --from=build /app/bin/server /app/bin/server

EXPOSE 8080

CMD ["/app/bin/server"]

Commands

# Run development server
dart run bin/server.dart

# Run with hot reload (using dart_dev or similar)
dart pub global activate webdev
webdev serve

# Run tests
dart test

# Generate code
dart run build_runner build

# Build executable
dart compile exe bin/server.dart -o server

# Format
dart format .

# Analyze
dart analyze

References