Routing

Routes in Finch are defined using FinchRoute objects collected inside a routing function. The routing function is registered with app.addRouting() and is called for every incoming request.

Routing Function

import 'package:finch/finch_route.dart';
import '../controllers/home_controller.dart';

final homeController = HomeController();

Future<List<FinchRoute>> getWebRoute() async {
  return [
    FinchRoute(
      key: 'root',
      path: '/',
      methods: Methods.ONLY_GET,
      index: homeController.index,
    ),
  ];
}

Register it in app.dart:

app.addRouting(getWebRoute);

FinchRoute Parameters

Parameter Type Description
key String Unique route identifier. Used to generate URLs ($e.routeUrl('key'))
path String URL path segment. Supports {param} or :param and wildcard *
methods List<String> Allowed HTTP methods. Use Methods constants
index Future<String> Function()? Handler function (no rq parameter; use Context.rq or a controller method)
controller Controller? Controller instance. Its index() is called when no index is set
children List<FinchRoute> Nested child routes. Path is relative to parent
extraPath List<String> Additional paths that map to the same route
auth AuthController? Authentication guard for this route
permissions List<String> Permission strings checked after auth
middlewares List<Middleware> Middleware chain executed before the handler
hosts List<String> Restrict to specific hostnames (['*'] = all)
ports List<int> Restrict to specific ports ([] = all)
params Map<String, dynamic> Default template parameters
excludePaths List<String> Sub-paths excluded from this route
apiDoc Future<ApiDoc>? Function()? API documentation for Swagger
widget String Path to a template file to render directly, with no controller or index needed (see Widget Routes below)
title String Page title, available in templates as {{ $e.pageTitle }}

Use only one of widget, index, or controller per route. If more than one is set, widget is checked first, then index, then controller.index() as a fallback.

HTTP Methods

// Convenience constants from Methods class:
Methods.ONLY_GET    // ['GET']
Methods.ONLY_POST   // ['POST']
Methods.ONLY_PUT    // ['PUT']
Methods.ONLY_DELETE // ['DELETE']
Methods.GET_POST    // ['POST', 'GET']
Methods.ALL         // All standard HTTP methods

// Or a custom list:
methods: [Methods.GET, Methods.POST, Methods.DELETE]

Path Parameters

Use {name} syntax to capture URL segments. Read them in the controller using rq.getParam('name'):

FinchRoute(
  key: 'users.show',
  path: 'users/{id}', // or 'users/:id'
  methods: Methods.ONLY_GET,
  index: userController.show,
),

Note: Dynamic path segments can be defined using either {param} or :param syntax. Both are supported and function identically.

// In UserController:
Future<String> show() async {
  var id = rq.getParam('id');
  return rq.renderData(data: {'id': id});
}

Widget Routes

When a route only needs to render a template with no custom logic, set widget to a template path instead of writing a controller:

FinchRoute(
  key: 'about',
  path: 'about',
  title: 'About Us',
  widget: 'pages/about',
),

widget is checked before index and controller, and — unlike those two — it does not wait for this route's own auth to run first; only permissions inherited from an already-authenticated parent route are checked. If a widget route needs authentication of its own, attach auth to a parent route instead of relying on the auth field of the widget route itself, or use index/controller so the auth check runs normally.

Wildcard Paths and Exclusions

A path ending in * matches the route itself and every sub-path beneath it — useful for catch-all handlers such as a single-page app fallback or a generic file proxy:

FinchRoute(
  key: 'spa.fallback',
  path: 'app/*',
  index: homeController.spaIndex,
  excludePaths: ['app/api'], // still falls through to other routes for this sub-path
),

excludePaths lists sub-paths that should not be swallowed by the wildcard, so more specific routes (or static files) underneath the wildcard can still be reached.

Nested Routes (children)

Child route paths are relative to the parent path:

FinchRoute(
  key: 'example',
  path: 'example',
  index: homeController.redirectToRoot,
  children: [
    FinchRoute(
      key: 'example.form.get',
      path: 'form',
      methods: Methods.ONLY_GET,
      index: homeController.exampleForm,
    ),
    FinchRoute(
      key: 'example.form.post',
      path: 'form',
      methods: Methods.ONLY_POST,
      index: authController.loginPost,
    ),
    FinchRoute(
      key: 'example.panel',
      path: 'panel',
      methods: Methods.ALL,
      auth: authController,
      permissions: ['admin'],
      index: homeController.exampleAuth,
    ),
  ],
),

Extra Paths

extraPath maps additional URL prefixes to the same route and its children:

FinchRoute(
  key: 'root.mysql',
  path: 'example/mysql',
  extraPath: ['api/example/mysql'],
  methods: Methods.GET_POST,
  index: homeController.exampleMysql,
),

Creating Multiple Similar Routes

FinchRoute.makeList creates several routes that share the same configuration but differ only by path — handy for aliases or near-identical endpoints:

var routes = FinchRoute.makeList(
  paths: ['users', 'members', 'people'],
  methods: Methods.ONLY_GET,
  controller: UserController(),
  auth: AppAuthController(),
  permissions: ['user.read'],
  key: 'user.alias', // becomes 'user.alias.1', 'user.alias.2', 'user.alias.3'
);

Middleware

Attach one or more Middleware instances to a route (or a fluent .middleware() chain) to run request-filtering logic before the handler executes — logging, header checks, rate limiting, CORS, and similar cross-cutting concerns. See the Middleware Guide for the full API and execution order relative to auth and permissions.

Host and Port Filtering

Restrict a route to specific hosts or ports:

FinchRoute(
  key: 'root.localhost',
  path: 'example/host',
  hosts: ['localhost'],
  ports: [80, 8085],
  index: homeController.renderLocalhost,
  methods: Methods.ALL,
),
FinchRoute(
  key: 'root.host',
  path: 'example/host',
  ports: [80, 8085],
  hosts: ['127.0.0.1'],
  index: homeController.render127001,
  methods: Methods.ALL,
),

Authentication Guard

Attach an AuthController to protect a route. If auth() returns false, the request is rejected before reaching the handler:

FinchRoute(
  key: 'admin.panel',
  path: 'admin/panel',
  auth: AppAuthController(),
  permissions: ['admin'],
  index: adminController.panel,
),

See Auth Controller for implementation details.

Route Caching

Use the .cache() extension to cache responses:

FinchRoute(
  key: 'root.route',
  path: 'example/route',
  methods: Methods.ONLY_GET,
  index: homeController.exampleRoute,
).cache(
  cacheDuration: Duration(minutes: 10),
  cacheType: [CacheParam.path, CacheParam.method, CacheParam.language],
  cacheSource: CacheSource.file,
),

See Route Cache for full details.

Generating URLs from Route Keys

In templates, use $e.routeUrl('key') to generate URLs:

<a href="{{ $e.routeUrl('example.panel') }}">Panel</a>
<a href="{{ $e.routeUrl('users.show', {'id': user.id}) }}">View User</a>

Get All Routes

Retrieve the full route list at runtime:

var routes = await app.getAllRoutes();

How Matching Works

For every incoming request, Finch walks the routing list you registered with app.addRouting() in order, top to bottom:

  1. Host/port filter — a route is skipped immediately if hosts or ports don't match the request.
  2. Path match — the route's path (plus each extraPath) is compared against the request path, resolving {param}/:param placeholders and wildcard * suffixes along the way.
  3. If the path matches but methods doesn't include the request's HTTP method, the route is treated as not found and matching continues to the next route.
  4. If the route has children, Finch recurses into them once the parent's path prefix matches — auth and middlewares on the parent still run first and apply to every child.
  5. The first route that fully matches (path, method, host, port, auth, permissions, middleware) handles the request; no further routes are tried.
  6. If nothing matches, Finch falls back to serving a static file from publicDir, and returns a 404 if that file doesn't exist either.

Because matching is first-match-wins, order matters: put more specific routes before broader wildcard routes that could otherwise swallow them.