Skip to content

Routing

Routing is at the heart of any web framework. Arcade provides a clean, Express-like routing API that’s both powerful and easy to use.

Routes in Arcade are defined using the global route object:

route.<method>(path).handle(handler);

Arcade supports all standard HTTP methods:

route.get('/users').handle((context) => 'GET users');
route.post('/users').handle((context) => 'POST user');
route.put('/users/:id').handle((context) => 'PUT user');
route.delete('/users/:id').handle((context) => 'DELETE user');
route.patch('/users/:id').handle((context) => 'PATCH user');
route.head('/users').handle((context) => 'HEAD users');
route.options('/users').handle((context) => 'OPTIONS users');

There’s also a special any method that matches all HTTP methods:

route.any('/api/*').handle((context) => 'Matches any method');

Capture dynamic segments in your URLs using the :param syntax:

route.get('/users/:id').handle((context) {
final userId = context.pathParameters['id'];
return {'userId': userId};
});
// Multiple parameters
route.get('/posts/:postId/comments/:commentId').handle((context) {
final postId = context.pathParameters['postId'];
final commentId = context.pathParameters['commentId'];
return {
'postId': postId,
'commentId': commentId,
};
});

Use wildcards to match multiple path segments:

// Matches /files/image.jpg, /files/docs/report.pdf, etc.
route.get('/files/*').handle((context) {
final path = context.path;
return {'requestedFile': path};
});

Access query string parameters through the context:

// GET /search?q=arcade&limit=10
route.get('/search').handle((context) {
final query = context.queryParameters['q'] ?? '';
final limit = int.tryParse(context.queryParameters['limit'] ?? '20') ?? 20;
return {
'query': query,
'limit': limit,
};
});

Organize related routes using groups:

route.group<RequestContext>('/api/v1', defineRoutes: (route) {
// All routes in this group will be prefixed with /api/v1
route().get('/users').handle((context) => 'List users');
route().post('/users').handle((context) => 'Create user');
// Nested groups
route().group<RequestContext>('/admin', defineRoutes: (route) {
// This will be /api/v1/admin/dashboard
route().get('/dashboard').handle((context) => 'Admin dashboard');
});
});

Apply hooks to all routes in a group:

route.group<RequestContext>(
'/api',
before: [
(context) {
// This runs before all routes in the group
print('API request: ${context.path}');
return context;
},
],
after: [
(context, result) {
// This runs after all routes in the group
print('API response sent');
return (context, result);
},
],
defineRoutes: (route) {
route().get('/users').handle((context) => []);
route().get('/posts').handle((context) => []);
},
);

Routes are matched in the order they are defined. More specific routes should be defined before generic ones:

// Define specific routes first
route.get('/users/me').handle((context) => 'Current user');
route.get('/users/:id').handle((context) => 'User by ID');
// Generic wildcard routes last
route.get('/users/*').handle((context) => 'Other user routes');

Define a custom handler for 404 errors:

route.notFound((context) {
context.statusCode = 404;
return {
'error': 'Not Found',
'path': context.path,
'timestamp': DateTime.now().toIso8601String(),
};
});

Attach metadata to routes for documentation or other purposes:

route.get(
'/api/users',
extra: {
'description': 'List all users',
'auth': true,
'roles': ['admin', 'user'],
},
).handle((context) {
// Access metadata
final metadata = context.route.metadata?.extra;
return {'users': []};
});

For complex applications, organize routes in separate functions:

void defineUserRoutes() {
route.group<RequestContext>('/users', defineRoutes: (route) {
route().get('/').handle(listUsers);
route().get('/:id').handle(getUser);
route().post('/').handle(createUser);
route().put('/:id').handle(updateUser);
route().delete('/:id').handle(deleteUser);
});
}
void defineAuthRoutes() {
route.post('/login').handle(login);
route.post('/logout').handle(logout);
route.post('/register').handle(register);
}
// In your main function
await runServer(
port: 3000,
init: () {
defineUserRoutes();
defineAuthRoutes();
},
);

Routes can be registered dynamically based on configuration:

final features = ['users', 'posts', 'comments'];
for (final feature in features) {
route.get('/$feature').handle((context) => 'List $feature');
route.post('/$feature').handle((context) => 'Create $feature');
}

Arcade validates routes at startup:

  • Duplicate route definitions are allowed (last one wins)
  • Routes without handlers will cause an error
  • Invalid path patterns will be caught early

When using route groups, it’s recommended to specify the generic type parameter for better type inference:

route.group<RequestContext>('/api/v1', defineRoutes: (route) {
// The route parameter here will have proper type inference
route().get('/users').handle((context) => 'List users');
});

Why use the generic type?

  • Improves IDE autocomplete and type checking
  • Enables better type inference in the defineRoutes function
  • Makes the code more explicit and self-documenting
  • Helps prevent type-related runtime errors

Without generic type:

route.group('/api', defineRoutes: (route) {
// route parameter may have limited type inference
route().get('/users').handle((context) => 'Users');
});

With generic type:

route.group<RequestContext>('/api', defineRoutes: (route) {
// route parameter has full type inference
route().get('/users').handle((context) => 'Users');
});

This pattern is especially important when using custom context types or when working with hooks that transform the context type.

  1. Organize routes logically - Use groups and separate functions
  2. Be consistent with naming - Use RESTful conventions
  3. Define specific routes first - More generic patterns last
  4. Use meaningful path parameters - :userId instead of :id
  5. Handle errors appropriately - Define not found and error handlers
  6. Use generic type annotations - Specify <RequestContext> for better type inference