Routing

Craft includes a highly flexible and expressive routing engine mapped via the Route facade. The routes map incoming HTTP request methods and paths directly to controller actions or closure handlers.

Routes are defined inside the routes/ directory:

The starter application's canonical login route is GET /login (named login), with credentials submitted to POST /login (named login.attempt). A GET /signin request redirects to /login for links that use that spelling. New links should use the named login route; the redirect does not handle login submissions. See Authentication for the complete starter route contract and middleware flow.


Basic Routes

You map paths to a controller action using a list with the controller class and the string action name:

from craft.facades import Route
from app.Http.Controllers.Blog.PostController import PostController

# Basic GET route
Route.get("/posts", [PostController, "index"]).name("posts.index")

# POST route
Route.post("/posts", [PostController, "store"]).name("posts.store")

Other supported HTTP verbs include:

Route.put("/posts/{id}", [PostController, "update"])
Route.patch("/posts/{id}", [PostController, "patch"])
Route.delete("/posts/{id}", [PostController, "destroy"])

Route Parameters

You capture dynamic path parameters using curly braces {}. These are parsed and passed as keyword arguments to the mapped controller action:

# Route mapping
Route.get("/posts/{id}", [PostController, "show"]).name("posts.show")

# Controller Action
class PostController(Controller):
    def show(self, request, id: str):
        post = Post.find_or_fail(id)
        return self.view("posts.show", {"post": post})

Route Named Access

You can chain .name() to name a route. This decouples your HTML templates and controller redirects from hardcoded URLs. In Python code, generate URLs with Route.url_for(); inside Forge templates, the route() helper does the same:

from craft.facades import Route

# Generate path
url = Route.url_for("posts.show", id="some-uuid-value")  # '/posts/some-uuid-value'

url_for URL-encodes parameter values, raises ValueError when a path parameter is missing, and appends any extra parameters as a query string:

Route.url_for("posts.index", page=2)   # '/posts?page=2'

There is no global Python route() function — that helper exists only inside Forge templates.


Route Groups

Grouping allows you to apply bulk attributes—such as route prefixes, shared middleware, or name prefixes—to multiple routes at once:

Route.group(
    lambda: (
        Route.get("/dashboard", [AdminController, "index"]).name("dashboard"),
        Route.get("/settings", [AdminController, "settings"]).name("settings"),
    ),
    prefix="/admin",
    middleware=["auth"],
    name="admin.",
)

Resource Controllers

A single call to Route.resource maps standard RESTful operations on a resource to their corresponding controller actions:

Route.resource("posts", PostController)

The resource mapper registers the following routes:

Verb Path Action Route Name Description
GET /posts index posts.index List posts
GET /posts/create create posts.create Form to create post
POST /posts store posts.store Store a new post
GET /posts/{id} show posts.show Display a post
GET /posts/{id}/edit edit posts.edit Form to edit post
PUT /posts/{id} update posts.update Update a post
DELETE /posts/{id} destroy posts.destroy Delete a post

The resource mapper registers PUT only for updates — register a Route.patch(...) route yourself if you also want PATCH. Parameters use {id}, so the action signature is def update(self, request, id).

Use Route.api_resource to exclude create and edit routes when mapping REST APIs.

Fallback Routes

A fallback catches whatever no other route claimed — a single-page app's index, a friendly 404, a per-tenant resolver:

Route.fallback("/{path:path}", SpaController.index)

It is registered like any route but always matched last, whatever order it was registered in. A catch-all placed by registration order would shadow every route declared after it, so the kernel builds the route table with the fallbacks at the end. Route.fallback(path, action, methods) defaults to GET and HEAD; pass methods explicitly for a fallback that must answer writes too.