Dart Frog Documentation

repository·main·Indexed 25 days ago

https://github.com/dart-frog-dev/dart_frog

A fast, minimalistic backend framework for Dart built on shelf and mason. It provides a unified tech stack for Flutter/Dart developers to build backends that aggregate, compose, and normalize data. The ecosystem includes the dart_frog_cli for project management, dart_frog_auth for header-based authentication, dart_frog_test for mocking RequestContext, and a dedicated VSCode extension for scaffolding routes and middleware.

Tokens
34.2K
Snippets
121
Records
164
Agent score
80%

What's inside Dart Frog

  1. What is middleware in Dart Frog

    main

    Middleware in Dart Frog allows you to execute code before and after a request is processed. It can be used to modify inbound requests, modify outbound responses, or provide dependencies to your routes.

    In Dart Frog, middleware is defined by a middleware function exported from a _middleware.dart file located within a subdirectory of the routes folder.

    There are two levels of middleware application:

    1. Global Middleware: A _middleware.dart file located directly in the routes/ directory is executed for all inbound requests.
    2. Route-specific Middleware: A _middleware.dart file located within a specific subdirectory (e.g., routes/hello/_middleware.dart) is executed only for requests matching that route and its children.
    import 'package:dart_frog/dart_frog.dart';
    
    Handler middleware(Handler handler) {
      return (context) async {
        // Execute code before request is handled.
    
        // Forward the request to the respective handler.
        final response = await handler(context);
    
        // Execute code after request is handled.
    
        // Return a response.
        return response;
      };
    }
  2. Authentication in Dart Frog

    main

    Dart Frog does not include built-in authentication features, helpers, or resources out of the box. This design provides developers with full freedom to implement any authentication protocol or service that fits their specific business logic.

    For a head start, you can use the dart_frog_auth package, which provides foundations for common authentication methods like Basic, Bearer, and Cookie-based authentication.

  3. Distinguish between Authentication and Authorization

    main

    The dart_frog_auth package handles Authentication (verifying who a user is). It does not handle Authorization (verifying what a user can do).

    • Authentication failure: The middleware automatically returns 401 Unauthorized if the authenticator returns null.
    • Authorization failure: You must manually implement checks in your handlers. If a user is authenticated but lacks permission for a specific action, you should return 403 Forbidden.

    Example of manual authorization check:

    Future<Response> _deleteUser(RequestContext context, String id) async {
      // If there is no authenticated user, `dart_frog_auth` automatically
      // responds with a 401.
    
      final user = context.read<User>();
      if (user.id != id) {
        // If the current authenticated user is not the owner of the resource,
        // return a forbidden response.
        return Response(statusCode: HttpStatus.forbidden);
      }
      await context.read<UserRepository>().deleteUser(user.id);
      return Response(statusCode: HttpStatus.noContent);
    }
  4. Use `provider` for Dependency Injection

    main

    In Dart Frog, you can inject dependencies into a RequestContext using provider middleware. A provider takes a create callback that is called lazily to produce an instance of type T. You can access these injected values within route handlers or other middleware using context.read<T>().

    To inject a value, use handler.use(provider<T>((context) => ...)) in your middleware. To retrieve it, use context.read<T>() in your onRequest handler.

    import 'package:dart_frog/dart_frog.dart';
    
    // Injecting a String
    Handler middleware(Handler handler) {
      return handler.use(provider<String>((context) => 'Welcome to Dart Frog!'));
    }
    
    // Accessing the String
    Response onRequest(RequestContext context) {
      final greeting = context.read<String>();
      return Response(body: greeting);
    }
  5. Use wildcard routes

    main

    Wildcard routes match any number of path segments. Use the [...name] syntax in the filename.

    • Example: routes/posts/[...page].dart will match /posts/today, /posts/features/starred, etc.
    • The captured path segments are passed as a single String argument to onRequest.

    Caution: Wildcard routes must be unique leaf routes on their route node. This means they must be a file and must be the only route in that specific folder.

    import 'package:dart_frog/dart_frog.dart';
    
    // For routes/posts/[...page].dart
    Response onRequest(RequestContext context, String page) {
      return Response(body: 'post page: $page');
    }
  6. Understand logging and progress event patterns in the Daemon

    main

    The Dart Frog daemon uses specific naming conventions for events to distinguish between operational logs and long-running task progress.

    Logging Events

    Logs are identified by the logger prefix followed by a severity level. The domain of the log is determined by the operation that generated it (e.g., dev_server.loggerInfo).

    SeverityEvent Identifier
    debugloggerDetail
    infologgerInfo, loggerSuccess, loggerWrite
    warnloggerWarning
    errorloggerError
    criticalloggerAlert

    Progress Events

    Progress events signal the lifecycle of long-running operations (like generating server code). They are identified by the progress prefix and include the following identifiers:

    • progressStart: Signals the start of an operation. Includes a progressId for tracking.
    • progressUpdate: Signals an update in progress.
    • progressCancel: Signals that progress was cancelled.
    • progressFail: Signals that progress failed.
    • progressComplete: Signals that progress completed successfully.
  7. Provide dependencies via middleware using the provider function

    main

    In Dart Frog, you can use middleware to inject dependencies into the RequestContext. This allows all subsequent route handlers to access the dependency using context.read<T>().

    To provide a dependency, use the provider<T> middleware within your routes/_middleware.dart file. It is common practice to instantiate the dependency once at the top level of the middleware file to ensure a single instance is shared across the application lifetime.

    import 'package:dart_frog/dart_frog.dart';
    import 'package:in_memory_todos_data_source/in_memory_todos_data_source.dart';
    
    final _dataSource = InMemoryTodosDataSource();
    
    Handler middleware(Handler handler) {
      return handler
          .use(requestLogger())
          .use(provider<TodosDataSource>((_) => _dataSource));
    }
  8. Integrate BroadcastCubit with WebSockets for real-time updates

    main

    You can combine package:broadcast_bloc with WebSockets to broadcast state changes to connected clients.

    1. Create a BroadcastCubit to manage state.
    2. Provide the Cubit via Dart Frog's provider and inject it into the request context using middleware.
    3. In the WebSocket handler, use context.read<YourCubit>() to access the instance.
    4. Use cubit.subscribe(channel) to link the WebSocket channel to the Cubit's state stream.
    5. Use cubit.unsubscribe(channel) in the onDone callback of the stream listener to clean up when the client disconnects.
    import 'package:dart_frog/dart_frog.dart';
    import 'package:dart_frog_web_socket/dart_frog_web_socket.dart';
    import 'package:web_socket_counter/counter/counter.dart';
    
    Future<Response> onRequest(RequestContext context) async {
      final handler = webSocketHandler(
        (channel, protocol) {
          // Subscribe the new client to receive notifications whenever the cubit state changes.
          final cubit = context.read<CounterCubit>()..subscribe(channel);
    
          // Send the current count to the new client.
          channel.sink.add('${cubit.state}');
    
          // Listen for messages from the client.
          channel.stream.listen(
            (event) {
              switch (event) {
                case '__increment__':
                  cubit.increment();
                  break;
                case '__decrement__':
                  cubit.decrement();
                  break;
                default:
                  break;
              }
            },
            // Unsubscribe the channel when the client disconnects.
            onDone: () => cubit.unsubscribe(channel),
          );
        },
      );
    
      return handler(context);
    }