Skip to content
Back to skills

Controllers

ASecurity

Controllers in Remix v3 — the `.tsx` files under `app/controllers/` that handle requests. Covers the `Controller<typeof routes.X, AppContext>` shape, action handler signatures (`{ get, params, request, url }`), reading middleware-provided values with `get(Database)` / `get(Session)` / `get(Auth)` / `get(FormData)`, returning responses via `render(<Page/>)` / `redirect(...)` / `Response.json(...)`, nested controllers, per-controller middleware (`requireAuth`, etc.), and the typed `routes.X.hre...

  • 4 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 12, 2026
code-qualitygonextjsexpressrailsdebuggingapidatabase

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 12, 2026

npx -y skills add aibot88/sec_skill_store --skill controllers --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Controllers?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Controllers
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aibot88-controllers/badge)](https://www.skillsdirectory.com/skills/aibot88-controllers)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: remix-controllers
description: Controllers in Remix v3 — the `.tsx` files under `app/controllers/` that handle requests. Covers the `Controller<typeof routes.X, AppContext>` shape, action handler signatures (`{ get, params, request, url }`), reading middleware-provided values with `get(Database)` / `get(Session)` / `get(Auth)` / `get(FormData)`, returning responses via `render(<Page/>)` / `redirect(...)` / `Response.json(...)`, nested controllers, per-controller middleware (`requireAuth`, etc.), and the typed `routes.X.href(params)` URL builder. Load whenever the user is creating, editing, or debugging anything under `app/controllers/`, or comparing to Express route handlers / Next.js page+route handlers / Rails controllers / Hono handlers. Also load when the user asks "where does business logic go" or "how do I get the current user in an action".
---

# Controllers

A controller is a `.tsx` file that exports a `default { actions, middleware? } satisfies Controller<typeof routes.X, AppContext>`. Controllers mirror the shape of the route map and are bound to it in `app/router.ts` with `router.map(routes.X, controllerX)`.

## Imports

```ts
import type { Controller } from 'remix/fetch-router'
import { redirect } from 'remix/response/redirect'
import * as s from 'remix/data-schema'
import * as f from 'remix/data-schema/form-data'
import { Database } from 'remix/data-table'
import { Session } from 'remix/session'
import { Auth } from 'remix/auth-middleware'

import type { AppContext } from '../router.ts'
import { routes } from '../routes.ts'
import { render } from '../utils/render.tsx'
```

## Minimal controller

```tsx
// app/controllers/home.tsx
import type { Controller } from 'remix/fetch-router'
import type { AppContext } from '../router.ts'
import { routes } from '../routes.ts'
import { render } from '../utils/render.tsx'
import { HomePage } from '../views/home-page.tsx'

export default {
  actions: {
    index() {
      return render(<HomePage />)
    },
  },
} satisfies Controller<typeof routes.home, AppContext>
```

## Action signature

Each action receives an object with:

| Field      | Type / What it is                                  |
|-----------|----------------------------------------------------|
| `get`     | `<K>(key: Key<K>) => K` — pull values from context (e.g. `get(Database)`) |
| `params`  | Typed params parsed from the route pattern         |
| `request` | The raw Web `Request`                              |
| `url`     | The parsed `URL` of the request                    |

Return value is a `Response` (usually built via `render(<Page/>)`, `redirect(...)`, or a manual `new Response(...)`).

## Mirroring a route map

If a route map has nested entries, the controller mirrors that shape:

```tsx
// app/controllers/admin/books/controller.tsx
import type { Controller } from 'remix/fetch-router'
import * as s from 'remix/data-schema'
import * as f from 'remix/data-schema/form-data'
import { Database } from 'remix/data-table'
import { redirect } from 'remix/response/redirect'

import type { AppContext } from '../../../router.ts'
import { routes } from '../../../routes.ts'
import { books } from '../../../data/schema.ts'
import { render } from '../../../utils/render.tsx'

import { AdminBooksIndexPage } from './views/index-page.tsx'
import { AdminBookShowPage }  from './views/show-page.tsx'
import { AdminBookNewPage }   from './views/new-page.tsx'

const bookSchema = f.object({
  slug:          f.field(s.string()),
  title:         f.field(s.string()),
  author:        f.field(s.string()),
  description:   f.field(s.string()),
  price:         f.field(s.string()),
  genre:         f.field(s.string()),
  isbn:          f.field(s.string()),
  publishedYear: f.field(s.string()),
  cover:         f.field(s.string().nullable()),
  inStock:       f.field(s.boolean()),
})

export default {
  actions: {
    async index({ get }) {
      const db = get(Database)
      const allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
      return render(<AdminBooksIndexPage books={allBooks} />)
    },

    new() {
      return render(<AdminBookNewPage />)
    },

    async create({ get }) {
      const db = get(Database)
      const formData = get(FormData)
      const parsed = s.parseSafe(bookSchema, formData)
      if (!parsed.success) {
        return render(<AdminBookNewPage errors={parsed.issues} />, { status: 400 })
      }
      const { price, publishedYear, cover, ...rest } = parsed.value
      await db.create(books, {
        ...rest,
        price: parseFloat(price),
        cover_url: cover ?? '/images/placeholder.jpg',
        image_urls: JSON.stringify([]),
        published_year: parseInt(publishedYear, 10),
      })
      return redirect(routes.admin.books.index.href())
    },

    async show({ get, params }) {
      const db = get(Database)
      const book = await db.find(books, params.bookId)
      if (!book) return new Response('Not found', { status: 404 })
      return render(<AdminBookShowPage book={book} />)
    },
  },
} satisfies Controller<typeof routes.admin.books>
```

## Controller-level middleware

A controller can declare middleware that runs before *any* of its actions (and any nested sub-controllers). Useful for guards like `requireAuth` or for hydrating per-request state.

```tsx
import { requireAuth } from 'remix/auth-middleware'

export default {
  middleware: [requireAuth()],
  actions: {
    index({ get }) { /* … */ },
    settings: {
      // nested controller — its own middleware can stack
      middleware: [/* … */],
      actions: { /* … */ },
    },
  },
} satisfies Controller<typeof routes.account>
```

Execution order: global router middleware → matched controller chain (parent → child) → action.

## Reading typed context with `get(Key)`

Anything a middleware puts on the request context is fetched in actions via `get(Key)`. Common keys:

```ts
const db       = get(Database)   // remix/data-table — provided by your db middleware
const session  = get(Session)    // remix/session — provided by session() middleware
const formData = get(FormData)   // a parsed FormData — provided by formData() middleware
const auth     = get(Auth)       // current Auth state — provided by auth() middleware
```

If a key is missing from context the call throws. That's a wiring bug — fix the middleware stack in `app/router.ts`.

## Common return shapes

```tsx
// HTML page
return render(<Page />)

// HTML with a non-200 status (validation error, conflict, not-found)
return render(<NewBookPage errors={parsed.issues} />, { status: 400 })

// Redirect (after a successful POST, login, etc.)
return redirect(routes.admin.books.index.href())

// Bare response for 404s, JSON APIs, downloads, etc.
return new Response('Not found', { status: 404 })
return Response.json({ ok: true })
```

`routes.X.href(params?)` produces a typed URL. Always prefer it over hand-rolled paths — the compiler will tell you when you've broken a link.

## Returning HTML without JSX

For small endpoints (webhooks, RSS, email previews), skip the JSX pipeline and use the `html` tagged template:

```ts
import { html } from 'remix/html-template'
import { createHtmlResponse } from 'remix/response/html'

return createHtmlResponse(html`<h1>Hello ${name}</h1>`)
```

See [templating](../templating/SKILL.md) for the full story.

## Further reading

- `references/controller-types.md` — `Controller<Routes, Context>`, action shape, nested mirror rules
- `references/render-utility.md` — building `app/utils/render.tsx` around `renderToStream`
- `references/context-keys.md` — the standard set of `Key`s exposed by built-in middleware
- See also: [templating](../templating/SKILL.md) (HTML responses without JSX), [headers](../headers/SKILL.md) (typed parsers for Accept, CacheControl, etc.)

Files in this skill

  • SKILL.md7.7 KB
  • source.json1.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.