Patterns for declaring URLs, handling requests, and wiring routes to controllers. Read this when the task involves:
- Defining or changing the URL surface of the app
- Writing or reorganizing controllers and actions
- Reading request data (`params`, `url`, `request`, context values)
- Returning a `Response` for HTML, redirects, JSON, or errors
- Generating internal URLs with `.href()`
The companion reference for shaping `Request` bodies, validating input, and dealing with persisted data is `data-and-validation.md`. For request lifecycle and middleware ordering, see `middleware-and-server.md`.
## Route Builders
Import all route builders from `remix/routes`.
### `route(prefix, map)` — nested route group
Adds a URL prefix to all children. Can also be called as `route(map)` without a prefix for a top-level grouping. Inside `route(...)`, a nested map may be either a `route('prefix', { ... })` call (when you want a shared URL prefix) or a plain object literal (when each leaf already owns its absolute path).
```typescript
import{route,get,post}from'remix/routes'
exportconstroutes=route({
home:'/',
// Plain object — no shared prefix, each leaf has an absolute path.
books:{
index:'/books',
show:'/books/:slug',
},
// route('auth', ...) — every leaf is prefixed with /auth.
An action is the handler for one leaf route. In Remix app code, actions should live in controllers. Use `Action` only when a reusable helper needs to type one action before it is added to a controller or when you are doing low-level router wiring outside the `app/actions` convention:
For state-changing routes (POST, PUT, PATCH, DELETE), the canonical reply is a redirect to the resulting page. Pass `303` explicitly when you want a POST-redirect-GET flow:
For routes consumed by client code rather than rendered as a page (autocomplete endpoints, polling APIs, inter-service calls), return a JSON `Response`. Use `SuperHeaders` from `remix/headers` when typed header accessors make the response clearer:
If you find yourself returning JSON for what is really a browser form submission, prefer the redirect-after-POST pattern instead. JSON-only mutation endpoints make it harder to support non-JS clients, harder to share rendering logic, and easier for the client to drift out of sync with the server.
## Controllers
A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not belong inside a controller's `actions`; map those route maps with their own controllers.
Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(databaseContext)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
Because `account` is a nested route map, it is not an action key in the root controller.
### Nested route maps
Nested route maps use their own controllers under `app/actions/<route-key>/controller.tsx`. Directory names under `app/actions/` are route-map keys, not URL path segments.
The `middleware` array on a controller runs only for the direct actions in that controller, before action middleware. It does not apply to other controllers.
```typescript
exportdefaultcreateController(routes.admin,{
middleware:[requireAuth(),requireAdmin()],
actions:{
/* all actions require auth + admin */
},
})
```
## Registering Routes
Use `router.map` for route maps and controllers. Map each nested route map explicitly. Use verb methods only for low-level router wiring outside the `app/actions` controller convention.