Skip to content

Middleware

View raw .md

Middleware lets you inspect and filter navigation requests before they reach a route, making it the right place for authentication guards, permission checks, and pre-navigation side effects.

On this page

Introduction#

Middleware provide a convenient mechanism for inspecting and filtering navigation requests entering your application. For example, an authentication middleware verifies the user is signed in. If the user is not, the middleware redirects them to your application's login screen; otherwise it lets the request proceed further into the application. You write that one yourself (see below); the core ships AuthorizeMiddleware for Gate abilities.

Of course, additional middleware can be written to perform a variety of tasks besides authentication. Middleware are stored in the lib/app/middleware directory.

Generating Middleware#

Use dowel to generate a new middleware class:

bash
dart run magic:dowel make:middleware EnsureAuthenticated

This creates lib/app/middleware/ensure_authenticated.dart with a MagicMiddleware stub ready to implement.

Defining Middleware#

To create a new middleware, create a class that extends MagicMiddleware:

dart
import 'package:magic/magic.dart';

class EnsureAuthenticated extends MagicMiddleware {
  @override
  String? redirectTarget(String location) {
    if (!Auth.check() && location != '/login') return '/login';
    return null; // Authenticated, or already on /login: allow.
  }
}

As you can see, if the user is not authenticated, the middleware returns the login path and the user is redirected to the login screen. If the user is authenticated, it returns null and the request proceeds.

Redirect guards vs blocking guards#

Middleware has two hooks, and you pick the one that matches the intent:

  • redirectTarget(String location) for redirect-style guards (auth, guest, role gates). It is evaluated synchronously in the router's redirect callback BEFORE the route builds, so the destination view mounts exactly once. Return a path to redirect to, or null to allow. Always return null when location already equals the target, otherwise the redirect loops.
  • handle(void Function() next) for async, non-redirecting concerns (logging, a blocked-state screen). Call next() to allow, skip it to block. Both hooks default to allow, so override only the one you need.

Async Operations#

All middleware is asynchronous. You may perform await operations before deciding whether to call next():

dart
class TeamAccessMiddleware extends MagicMiddleware {
  @override
  Future<void> handle(void Function() next) async {
    final response = await Http.get('/teams/${Request.route('teamId')}/access');

    if (response.successful) {
      next();
    } else {
      MagicRoute.to('/unauthorized');
    }
  }
}

Registering Middleware#

Global Middleware#

If you want middleware to run during every navigation in your application, register it globally in your lib/app/kernel.dart:

dart
void registerKernel() {
  Kernel.global([
    () => LoggingMiddleware(),
    () => VerifyDeviceMiddleware(),
  ]);
}

Global middleware runs on every route, in the order they are registered.

Assigning Middleware To Routes#

To assign middleware to specific routes, first register it with a key in your Kernel:

dart
void registerKernel() {
  Kernel.registerAll({
    'auth': () => EnsureAuthenticated(),
    'guest': () => RedirectIfAuthenticated(),
    'admin': () => EnsureAdmin(),
  });
}

Then use the string key in your route definitions:

dart
// Single middleware
MagicRoute.page('/dashboard', () => DashboardView())
    .middleware(['auth']);

// Multiple middleware (executed in order)
MagicRoute.page('/admin', () => AdminPanel())
    .middleware(['auth', 'admin']);

An alias nothing registered stops the app at Magic.init, rather than leaving the route ungated:

text
StateError: Unresolvable route middleware on 1 route:
  /admin: Route middleware alias "admin" is not registered. Register it with
  Kernel.register('admin', () => ...) from a service provider.

The router checks every registered route's middleware when it builds, which is step 7 of Magic.init, after every provider has booted. So an alias registered from register() or from boot() is equally early, and the usual cause of this error is a typo or an alias nobody registered at all.

Every registered route includes the ones declared inside a layout. A route in MagicRoute.group(layout: ...) or MagicRoute.layout(...) is held in the layout's own child list rather than the top-level table, and a shell or tab layout is where a gated screen usually lives, so the check walks both.

Bootstrap rather than navigation is deliberate. Kernel.resolveAll throws at navigation too, but that throw happens inside GoRouter's redirect callback, which routes it to onException: the page renders nothing and the log reports a routing failure, which names the wrong problem.

Route Group Middleware#

You can apply middleware to all routes within a group:

dart
MagicRoute.group(
  middleware: ['auth'],
  routes: () {
    MagicRoute.page('/dashboard', () => DashboardView());
    MagicRoute.page('/profile', () => ProfileView());
    MagicRoute.page('/settings', () => SettingsView());
  },
);

Middleware & Responses#

Since middleware controls navigation flow, there are two possible outcomes:

Action Effect
Call next() Navigation proceeds to the route
Don't call next() Navigation is blocked

When blocking, you should redirect the user:

dart
@override
Future<void> handle(void Function() next) async {
  if (await hasValidSubscription()) {
    next();
  } else {
    // Don't call next() - redirect instead
    MagicRoute.to('/subscription/required');
    Magic.error('Subscription Required', 'Please upgrade your plan.');
  }
}

Authorization Middleware#

Magic includes AuthorizeMiddleware that integrates with the Gate authorisation system, the counterpart of Laravel's can middleware.

Basic Usage#

Register authorisation middleware in your Kernel:

dart
void registerKernel() {
  Kernel.registerAll({
    'auth': () => EnsureAuthenticated(),
    'can:manage-team': () => AuthorizeMiddleware('manage-team'),
    'can:admin': () => AuthorizeMiddleware('admin-access'),
  });
}

Use in your routes:

dart
MagicRoute.page('/team/settings', () => TeamSettings())
    .middleware(['auth', 'can:manage-team']);

MagicRoute.page('/admin', () => AdminPanel())
    .middleware(['auth', 'can:admin']);

Custom Redirect#

By default, unauthorised users are redirected to /unauthorized. Customise this:

dart
Kernel.register('can:admin', () => AuthorizeMiddleware(
  'admin-access',
  unauthorizedRoute: '/access-denied',
));

With Model Arguments#

For authorisation requiring a model (like checking ownership), create a custom middleware:

dart
class CanEditPostMiddleware extends MagicMiddleware {
  @override
  Future<void> handle(void Function() next) async {
    final response = await Http.show('posts', Request.route('id')!);

    if (response.successful && Gate.allows('edit-post', Post.fromMap(response.data))) {
      next();
    } else {
      MagicRoute.to('/unauthorized');
    }
  }
}

Register and use it:

dart
Kernel.register('can:edit-post', () => CanEditPostMiddleware());

MagicRoute.page('/posts/:id/edit', (id) => EditPostView(id: id))
    .middleware(['auth', 'can:edit-post']);

See the Authorization documentation for more on defining abilities and policies.

Gotchas#

Mistake Fix
A redirectTarget that does not return null when location already equals the target Return null for the target itself, otherwise the redirect loops
Redirecting with MagicRoute.to() from inside handle() Use redirectTarget: it runs before the route builds, so the destination mounts once instead of remounting
Using a route alias nobody registered Magic.init stops with a StateError naming the route and the alias; register it with Kernel.register or Kernel.registerAll
Passing an instance to Kernel.register It takes a factory: Kernel.register('auth', () => EnsureAuthenticated())
Skipping next() in handle() without redirecting The route renders an empty box in place of its page; send the user somewhere or show feedback
AuthorizeMiddleware with no /unauthorized route A failed ability redirects to unauthorizedRoute (default /unauthorized); register that route or pass your own
esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.