# Routing and Controllers

## What This Covers

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'

export const routes = 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.
  auth: route('auth', {
    login: get('login'),
    logout: post('logout'),
  }),
})
```

### Leaf route builders

| Builder        | HTTP method | Example              |
| -------------- | ----------- | -------------------- |
| `get(path)`    | GET         | `get('/search')`     |
| `post(path)`   | POST        | `post('/logout')`    |
| `put(path)`    | PUT         | `put('/api/update')` |
| `del(path)`    | DELETE      | `del('/api/remove')` |
| String literal | ANY         | `'/about'`           |

### `form(path, options?)` — form route

Creates a GET + POST pair for HTML form workflows. Expands to an `index` (GET) and an `action` (POST) by default.

```typescript
contact: form('contact')
// Produces routes.contact.index (GET /contact) and routes.contact.action (POST /contact)

settings: form('settings', { formMethod: 'PUT', names: { action: 'update' } })
// Produces routes.settings.index (GET) and routes.settings.update (PUT)
```

### `resources(name, options?)` — REST resources

Expands to conventional CRUD routes: `index`, `new`, `create`, `show`, `edit`, `update`, `destroy`.

```typescript
books: resources('books', { param: 'bookId' })
// GET /books, GET /books/new, POST /books, GET /books/:bookId, ...

orders: resources('orders', { only: ['index', 'show'], param: 'orderId' })
// GET /orders, GET /orders/:orderId
```

### URL generation with `.href()`

Route objects expose `.href()` for type-safe URL generation:

```typescript
redirect(routes.home.href())
redirect(routes.account.orders.show.href({ orderId: '42' }))
```

## Actions

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:

```typescript
import { createAction } from 'remix/router'

import { routes } from '../routes.ts'

export const search = createAction(routes.search, {
  async handler({ url }) {
    let query = url.searchParams.get('q') ?? ''
    let results = await searchIndex(query)
    return render(<SearchPage query={query} results={results} />)
  },
})
```

The handler receives a context object with:

- `get(key)` — read a value set by middleware (e.g. `get(Database)`, `get(Session)`, `get(Auth)`)
- `params` — typed route params
- `url` — the request URL
- `request` — the raw `Request`

Actions with action middleware:

```typescript
import { createAction } from 'remix/router'
import { requireAuth } from 'remix/middleware/auth'

export const account = createAction(routes.account.index, {
  middleware: [requireAuth()],
  handler(context) {
    return render(<AccountPage />)
  },
})
```

## Returning Responses

An action returns a `Response`. The shape of that response is part of the route contract, and choosing it well saves a lot of glue elsewhere.

### Render HTML

For pages, render a component tree and return the resulting `Response`:

```typescript
async handler({ get }) {
  let db = get(Database)
  let books = await db.findMany(books, { orderBy: ['id', 'asc'] })
  return render(<IndexPage books={books} />)
}
```

### Redirect after a mutation

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:

```typescript
import { redirect } from 'remix/response/redirect'

async create({ get }) {
  let formData = get(FormData)
  let parsed = s.parseSafe(bookSchema, formData)
  if (!parsed.success) {
    return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
  }

  let db = get(Database)
  let book = await db.create(books, parsed.value)

  return redirect(routes.books.show.href({ slug: book.slug }), 303)
}
```

This pattern works without JavaScript and stays compatible with `clientEntry(...)` enhancements on top.

### Return an error response

For expected failures — validation, conflict, not found — return a `Response` directly. Reserve thrown errors for genuinely unexpected failures.

```typescript
async show({ get, params }) {
  let db = get(Database)
  let book = await db.find(books, params.bookId)
  if (!book) return new Response('Not Found', { status: 404 })
  return render(<ShowPage book={book} />)
}
```

For form re-rendering with errors, return the page component with the parsed issues:

```typescript
let formData = get(FormData)
let parsed = s.parseSafe(signupSchema, formData)
if (!parsed.success) {
  return render(<SignupPage errors={parsed.issues} values={Object.fromEntries(formData)} />, {
    status: 400,
  })
}
```

### Return JSON

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:

```typescript
import Headers from 'remix/headers'

let headers = new Headers()
headers.contentType = { mediaType: 'application/json', charset: 'utf-8' }
headers.cacheControl = { noStore: true }

return new Response(JSON.stringify({ results }), {
  headers,
})
```

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(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.

```typescript
import { createController } from 'remix/router'

import { routes } from '../routes.ts'

export default createController(routes.books, {
  actions: {
    async index({ get }) {
      let db = get(Database)
      let items = await db.findMany(books, { orderBy: ['id', 'asc'] })
      return render(<IndexPage items={items} />)
    },

    async show({ get, params }) {
      let db = get(Database)
      let book = await db.find(books, params.bookId)
      if (!book) return new Response('Not Found', { status: 404 })
      return render(<ShowPage book={book} />)
    },
  },
})
```

### Root controller

The root route map uses `app/actions/controller.tsx` and owns only top-level leaf routes:

```typescript
// routes.ts
export const routes = route({
  assets: get('/assets/*path'),
  home: '/',
  account: route('account', {
    index: '/',
    settings: form('settings', { formMethod: 'PUT', names: { action: 'update' } }),
  }),
})

// app/actions/controller.tsx
export default createController(routes, {
  actions: {
    async assets({ request }) {
      return (await assetServer.fetch(request)) ?? new Response('Not Found', { status: 404 })
    },
    home() {
      return render(<HomePage />)
    },
  },
})
```

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.

```typescript
// app/actions/account/controller.tsx
export default createController(routes.account, {
  middleware: [requireAuth()],
  actions: {
    index() {
      return render(<AccountPage />)
    },
  },
})

// app/actions/account/settings/controller.tsx
export default createController(routes.account.settings, {
  middleware: [requireAuth()],
  actions: {
    index() {
      return render(<SettingsPage />)
    },
    update() {
      return redirect(routes.account.index.href(), 303)
    },
  },
})
```

Then map each route map explicitly:

```typescript
import rootController from './actions/controller.tsx'
import accountController from './actions/account/controller.tsx'
import accountSettingsController from './actions/account/settings/controller.tsx'

let router = createRouter({ middleware })

router.map(routes, rootController)
router.map(routes.account, accountController)
router.map(routes.account.settings, accountSettingsController)
```

### Controller middleware

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
export default createController(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.

```typescript
let router = createRouter({ middleware })

// Route maps → controllers
router.map(routes, rootController)
router.map(routes.contact, contactController)
router.map(routes.auth, authController)
router.map(routes.auth.login, authLoginController)
router.map(routes.admin, adminController)
router.map(routes.admin.books, adminBooksController)

// Leaf route → one-off action
router.get(routes.search, searchAction)
router.post(routes.logout, logoutAction)
```

## Typed Context

Define an `AppContext` type from your router, then make it the default context used by `createAction()` and `createController()`:

```typescript
import { createRouter, type RouterContext } from 'remix/router'

export const router = createRouter({
  middleware: [formData(), session(cookie, storage), loadDatabase(), loadAuth()],
})

export type AppContext = RouterContext<typeof router>

declare module 'remix/router' {
  interface RouterTypes {
    context: AppContext
  }
}
```

This gives typed `context.get(Database)`, `context.get(Session)`, `context.get(Auth)`, etc.
