Routing
Routing maps incoming HTTP requests to handler functions based on URL patterns and HTTP methods. Mage provides a flexible routing system with built-in security features and support for custom router implementations.
Quick Start
import { MageApp } from "@mage/app";
const app = new MageApp();
// Static route
app.get("/", (c) => c.text("Home"));
// Route with parameter
app.get("/users/:id", (c) => {
const userId = c.req.params.id;
return c.json({ id: userId });
});
// Wildcard route
app.get("/files/*", (c) => {
const path = c.req.wildcard;
return c.text(`File path: ${path}`);
});
Route Patterns
Routes are defined using URL patterns that match incoming request paths.
Static Routes
Static routes match exact paths:
app.get("/", (c) => c.text("Home"));
app.get("/about", (c) => c.text("About"));
app.get("/contact/form", (c) => c.text("Contact Form"));
Matches only the exact path. /about matches, but /about/team does not.
Route Parameters
Use :param syntax to capture dynamic segments from the URL:
app.get("/users/:id", (c) => {
const userId = c.req.params.id;
return c.json({ id: userId });
});
app.get("/posts/:postId/comments/:commentId", (c) => {
const postId = c.req.params.postId;
const commentId = c.req.params.commentId;
return c.json({ postId, commentId });
});
Key behaviors:
- Parameters automatically decode URL encoding
- Parameters are validated to prevent path traversal attacks
- Each parameter name must be unique within a route
- Parameters cannot be empty (
/users/does not match/users/:id)
Wildcard Routes
Use * to match any remaining path segments:
app.get("/files/*", (c) => {
const filePath = c.req.wildcard;
return c.text(`Accessing: ${filePath}`);
});
app.get("/api/v1/*", (c) => {
const endpoint = c.req.wildcard;
return c.json({ endpoint });
});
Wildcard rules:
- Wildcard must be the last segment in the route pattern
- Matches zero or more path segments
- Access matched path via
c.req.wildcard - Only one wildcard allowed per route
// Valid
app.get("/static/*", handler);
app.get("/api/v1/*", handler);
// Invalid - wildcard must be last
app.get("/*/admin", handler); // Error at registration
Accessing Route Data
Parameters
Parameters are available on c.req.params:
app.get("/users/:userId/posts/:postId", (c) => {
const { userId, postId } = c.req.params;
return c.json({ userId, postId });
});
// GET /users/alice/posts/42
// → { userId: "alice", postId: "42" }
Wildcard
Wildcard paths are available on c.req.wildcard:
app.get("/docs/*", (c) => {
const docPath = c.req.wildcard;
return c.text(`Documentation: ${docPath}`);
});
// GET /docs/api/routing.html
// → "Documentation: api/routing.html"
Route Matching
Mage uses the LinearRouter by default, which matches routes using a linear
search algorithm.
How LinearRouter Works
- Normalize path: Removes consecutive slashes, ensures leading slash
- Split into segments: Both route pattern and request path
- Match segment by segment:
- Static segments must match exactly
- Parameters (
:name) extract and validate the segment - Wildcard (
*) matches all remaining segments
- Verify complete match: No extra segments in request path
// Route: /users/:id/posts
// Path: /users/42/posts
// Matching process:
// 1. Normalize: /users/42/posts (already normalized)
// 2. Split: ["", "users", "42", "posts"]
// 3. Match:
// - "" === "" ✓
// - "users" === "users" ✓
// - ":id" extracts "42" ✓
// - "posts" === "posts" ✓
// 4. Same length ✓
// Result: Match with params.id = "42"
Performance Characteristics
LinearRouter uses O(n) linear search:
- Iterates through all registered routes until a match is found
- Best for applications with fewer than 100 routes
- Simple, predictable, and easy to debug
- No preprocessing or tree construction overhead
// With 50 routes, worst case checks all 50
// Still completes in microseconds for typical apps
Route Priority
When multiple routes could match a path, Mage selects the most specific route based on a scoring system:
Specificity scores (per segment):
- Static segment: 3 points (highest priority)
- Parameter segment (
:param): 2 points - Wildcard segment (
*): 1 point (lowest priority)
app.get("/users/admin", handler1); // Score: 6 (3 + 3)
app.get("/users/:id", handler2); // Score: 5 (3 + 2)
app.get("/*", handler3); // Score: 1
// GET /users/admin → handler1 (most specific)
// GET /users/42 → handler2
// GET /anything/else → handler3
This ensures that exact matches take precedence over parameterized routes, and parameterized routes take precedence over wildcards.
Path Normalization
Mage automatically normalizes paths to prevent confusion attacks and match standard web server behavior.
Normalization Rules
- Remove consecutive slashes:
//→/ - Remove trailing slashes (except root)
- Ensure leading slash
// All of these normalize to the same path:
"/users/42" → "/users/42"
"//users//42" → "/users/42"
"/users/42/" → "/users/42"
"///users///42" → "/users/42"
// Root path remains unchanged:
"/" → "/"
This prevents attackers from using path variations to bypass security rules:
// Without normalization, these would be different:
app.get("/admin/users", requireAuth);
// Attacker tries: //admin//users
// With normalization: Both resolve to /admin/users
Path Traversal Protection
Mage validates all route parameters to prevent path traversal attacks.
Validation Process
When a parameter is extracted:
- URL decode the parameter value
- Check for dangerous patterns (case-insensitive)
- Reject request if path traversal detected
app.get("/files/:filename", (c) => {
// c.req.params.filename is already validated
const filename = c.req.params.filename;
return c.text(`File: ${filename}`);
});
// Safe requests:
// GET /files/document.pdf → filename = "document.pdf"
// GET /files/my%20file.txt → filename = "my file.txt"
// Blocked requests (400 Bad Request):
// GET /files/../etc/passwd
// GET /files/..%2Fetc%2Fpasswd
// GET /files/%2e%2e/secrets
Detected Patterns
Mage detects these path traversal patterns:
../- Basic Unix path traversal..\- Windows path traversal%2e%2e/- URL encoded../%2e%2e\- URL encoded..\..%2f- Partially encoded../..%5c- Partially encoded..\
Detection is case-insensitive to prevent evasion attempts.
Route Validation
Routes are validated when registered to catch errors early.
Format Validation
// Valid routes
app.get("/", handler);
app.get("/users", handler);
app.get("/users/:id", handler);
app.get("/files/*", handler);
// Invalid routes (throw MageError)
app.get("", handler); // Empty route
app.get("users", handler); // Missing leading /
app.get("/users//posts", handler); // Consecutive slashes
app.get("/*/admin", handler); // Wildcard not at end
app.get("/users/:", handler); // Empty parameter name
Duplicate Detection
Mage prevents duplicate routes that would cause parameter conflicts:
app.get("/users/:id", handler1);
app.get("/users/:userId", handler2); // Error: Duplicate route
// Both routes match /users/42
// Only the last match's params would be available
// Mage prevents this by detecting functional duplicates
Routes are considered duplicates if they have the same pattern structure, even with different parameter names:
// These are functionally identical:
"/users/:id" → "/users/:param"
"/users/:userId" → "/users/:param"
// Error: Duplicate detected
Custom Routers
Custom routing strategies are possible by implementing the MageRouter
interface. Pass to MageApp via new MageApp({ router: customRouter }). For
most applications, LinearRouter provides excellent performance.
Related
- MageApp - Main application class and route registration
- Middleware - Middleware patterns and execution order
- MageContext - Accessing request data and parameters
- Error Handling - Handling route errors