Middleware Guide

This guide will walk you through the steps to create and use middleware in your Finch application. Middleware provides a convenient mechanism for inspecting and filtering HTTP requests entering your application.

What is Middleware?

Middleware in Finch is a class that sits between the incoming HTTP request and your route handler (controller or index function). It allows you to run logic before a request reaches your controller — for example, checking headers, logging requests, validating tokens, modifying request parameters, or blocking unauthorized access.

Each middleware class must extend the abstract Middleware class and implement the handle() method.

How Middleware Works

When a request matches a route that has middleware attached, Finch executes the middleware pipeline in order before passing the request to the controller or index function. The flow is:

  1. Route pattern matching
  2. HTTP method, host, and port validation
  3. Authentication check (auth)
  4. Middleware execution (runs each middleware's handle() method in sequence)
  5. Permission check
  6. Controller/index execution

Return Values

The handle() method returns a Future<String?>:

  • Return null — the request passes through and continues to the next middleware or to the route handler.
  • Return a non-empty String — the request is blocked. The middleware pipeline stops, and the route is not matched.

If multiple middlewares are attached to a route, they are executed sequentially. If any middleware returns a non-empty string, the remaining middlewares are skipped and the request is rejected.

Creating a Middleware

To create a middleware, extend the Middleware class and override the handle() method:

import 'package:finch/finch_route.dart';

class MyMiddleware extends Middleware {
  @override
  Future<String?> handle() async {
    // Your logic here
    return null; // Allow the request to proceed
  }
}

Accessing the Request

Inside a middleware, you can access the current request using rq (provided by the Middleware base class):

class LogMiddleware extends Middleware {
  @override
  Future<String?> handle() async {
    print('Request: ${rq.method} ${rq.uri.path}');
    return null;
  }
}

Adding Parameters

You can inject data into the request from a middleware using rq.addParam(). This data will be available to controllers and templates downstream:

class TestMiddleware extends Middleware {
  @override
  Future<String?> handle() async {
    rq.addParam('middleware', 'Test Middleware Active');
    return null;
  }
}

Blocking a Request

Return a non-empty string to block the request from reaching the route handler:

class ApiKeyMiddleware extends Middleware {
  @override
  Future<String?> handle() async {
    final apiKey = rq.header('X-API-Key');
    if (apiKey != 'my-secret-key') {
      rq.renderError(401, message: 'Invalid API key');
      return 'Unauthorized'; // Blocks the request
    }
    return null; // Allow the request to proceed
  }
}

Attaching Middleware to Routes

There are two ways to attach middleware to a route.

1. Using the Constructor

Pass a list of middleware instances to the middlewares parameter of FinchRoute:

final testMiddleware = TestMiddleware();
final logMiddleware = LogMiddleware();

FinchRoute(
  path: '/info',
  index: homeController.info,
  middlewares: [testMiddleware, logMiddleware],
);

2. Using the Fluent API

Use the .middleware() method to chain middleware onto a route:

FinchRoute(
  path: '/info',
  index: homeController.info,
).middleware(TestMiddleware()).middleware(LogMiddleware());

Middleware on Parent Routes

When middleware is attached to a parent route that has children, the middleware runs before any child route is processed. This allows you to apply shared logic (such as authentication or logging) to an entire group of routes:

FinchRoute(
  path: '/admin',
  middlewares: [AdminMiddleware()],
  children: [
    FinchRoute(
      path: '/dashboard',
      index: adminController.dashboard,
    ),
    FinchRoute(
      path: '/users',
      index: adminController.users,
    ),
  ],
);

In this example, AdminMiddleware will execute before any request to /admin/dashboard or /admin/users.

Practical Examples

Rate Limiting Middleware

class RateLimitMiddleware extends Middleware {
  static final Map<String, List<DateTime>> _requests = {};
  final int maxRequests;
  final Duration window;

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

  @override
  Future<String?> handle() async {
    final ip = rq.clientIP;
    final now = DateTime.now();
    _requests[ip] = (_requests[ip] ?? [])
      ..removeWhere((t) => now.difference(t) > window)
      ..add(now);

    if (_requests[ip]!.length > maxRequests) {
      rq.renderError(429, message: 'Too many requests');
      return 'Rate limit exceeded';
    }
    return null;
  }
}

CORS Middleware

class CorsMiddleware extends Middleware {
  final String allowedOrigin;

  CorsMiddleware({this.allowedOrigin = '*'});

  @override
  Future<String?> handle() async {
    rq.response.headers.add('Access-Control-Allow-Origin', allowedOrigin);
    rq.response.headers.add('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    rq.response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    return null;
  }
}

Maintenance Mode Middleware

class MaintenanceMiddleware extends Middleware {
  final bool isEnabled;

  MaintenanceMiddleware({this.isEnabled = false});

  @override
  Future<String?> handle() async {
    if (isEnabled) {
      rq.renderError(503, message: 'Service is under maintenance');
      return 'Maintenance mode';
    }
    return null;
  }
}

Middleware vs Auth Controller

Feature Middleware Auth Controller
Purpose General-purpose request filtering Authentication & authorization
Execution Order After auth, before permissions Before middleware
Return Type Future<String?> Future<bool>
Can modify request Yes (rq.addParam) Yes
Can render errors Yes Yes
Multiple per route Yes (list) One per route
Inherited by children Yes Yes

Use Auth Controller when you need full authentication and session management. Use Middleware for everything else — logging, header validation, rate limiting, CORS, feature flags, etc.

Full Example

Here is a complete example of using middleware in a Finch application:

1. Create middleware classes:

// lib/middleware/test_middleware.dart
import 'package:finch/finch_route.dart';

class TestMiddleware extends Middleware {
  @override
  Future<String?> handle() async {
    rq.addParam('middleware', 'Test Middleware Active');
    return null;
  }
}

2. Register middleware in your routes:

// lib/route/web_route.dart
import '../middleware/test_middleware.dart';

final testMiddleware = TestMiddleware();

List<FinchRoute> routes = [
  FinchRoute(
    key: 'root.info',
    path: 'info',
    extraPath: ['api/info'],
    index: homeController.info,
    middlewares: [testMiddleware],
  ),
];

3. Access middleware data in your controller:

class HomeController extends Controller {
  Future<String> info() async {
    final middlewareParam = rq.get('middleware');
    return rq.renderString(text: 'Middleware says: $middlewareParam');
  }
}

API Reference

Middleware (abstract class)

Member Type Description
rq Request (getter) Access the current HTTP request object
handle() Future<String?> Override to implement middleware logic. Return null to pass, non-empty string to block.

FinchRoute Middleware Members

Member Type Description
middlewares List<Middleware> List of middleware instances attached to the route
middleware(m) FinchRoute Fluent method to add a middleware; returns the route for chaining
handleMiddlewares() Future<bool> Executes all middleware in order; returns true if all passed