Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/remix/llms.txt
Use this file to discover all available pages before exploring further.
Set up your router
First, create a router with middleware. The router handles all HTTP requests and applies middleware in order.import { createRouter } from 'remix/fetch-router'
import { logger } from 'remix/logger-middleware'
import { compression } from 'remix/compression-middleware'
let middleware = []
if (process.env.NODE_ENV === 'development') {
middleware.push(logger())
}
middleware.push(compression())
export let router = createRouter({ middleware })
The createRouter function creates a router instance that you can map routes to. Middleware runs for every request before your route handlers. Define your routes
Create a type-safe routes definition using the routes helper. This provides autocomplete and type checking.import { routes } from 'remix/fetch-router/routes'
export let apiRoutes = routes({
users: {
index: 'GET /api/users',
show: 'GET /api/users/:id',
create: 'POST /api/users',
update: 'PATCH /api/users/:id',
delete: 'DELETE /api/users/:id',
},
posts: {
index: 'GET /api/posts',
show: 'GET /api/posts/:id',
create: 'POST /api/posts',
},
})
Each route maps to an HTTP method and path pattern. Path parameters like :id are automatically extracted and typed. Create a controller
Controllers group related route handlers together. Each action receives a request context with typed params.import type { Controller } from 'remix/fetch-router'
import { apiRoutes } from './routes.ts'
// Sample in-memory database
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
]
export default {
actions: {
// GET /api/users - List all users
index() {
return Response.json(users)
},
// GET /api/users/:id - Get a single user
show({ params }) {
let user = users.find(u => u.id === Number(params.id))
if (!user) {
return Response.json(
{ error: 'User not found' },
{ status: 404 }
)
}
return Response.json(user)
},
// POST /api/users - Create a new user
async create({ request }) {
let body = await request.json()
let newUser = {
id: users.length + 1,
name: body.name,
email: body.email,
}
users.push(newUser)
return Response.json(newUser, { status: 201 })
},
// PATCH /api/users/:id - Update a user
async update({ params, request }) {
let user = users.find(u => u.id === Number(params.id))
if (!user) {
return Response.json(
{ error: 'User not found' },
{ status: 404 }
)
}
let body = await request.json()
Object.assign(user, body)
return Response.json(user)
},
// DELETE /api/users/:id - Delete a user
delete({ params }) {
let index = users.findIndex(u => u.id === Number(params.id))
if (index === -1) {
return Response.json(
{ error: 'User not found' },
{ status: 404 }
)
}
users.splice(index, 1)
return new Response(null, { status: 204 })
},
},
} satisfies Controller<typeof apiRoutes.users>
The satisfies operator ensures your controller matches the routes structure, providing full type safety. Map controllers to routes
Connect your controller to the router using the map method:import { createRouter } from 'remix/fetch-router'
import { apiRoutes } from './routes.ts'
import usersController from './users.ts'
import postsController from './posts.ts'
export let router = createRouter()
// Map the entire users controller
router.map(apiRoutes.users, usersController)
// Map the entire posts controller
router.map(apiRoutes.posts, postsController)
You can also map individual routes:router.get(apiRoutes.users.index, async () => {
return Response.json({ users: [] })
})
Add request validation
Use middleware to validate requests before they reach your handlers:app/middleware/validate.ts
import type { Middleware } from 'remix/fetch-router'
export function validateJson(): Middleware {
return async ({ request }, next) => {
let contentType = request.headers.get('content-type')
if (request.method !== 'GET' && !contentType?.includes('application/json')) {
return Response.json(
{ error: 'Content-Type must be application/json' },
{ status: 415 }
)
}
return next()
}
}
Apply it to specific controllers:import { validateJson } from './middleware/validate.ts'
export default {
middleware: [validateJson()],
actions: {
// ... your actions
},
} satisfies Controller<typeof apiRoutes.users>
Handle errors
Add error handling middleware to catch and format errors:import type { Middleware } from 'remix/fetch-router'
export function errorHandler(): Middleware {
return async (context, next) => {
try {
return await next()
} catch (error) {
console.error('Request error:', error)
return Response.json(
{
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
}
Add it as the first middleware:import { errorHandler } from './middleware/errors.ts'
let middleware = [
errorHandler(),
logger(),
compression(),
]
export let router = createRouter({ middleware })
Start your server
Create a server to handle requests:import { createServer } from 'remix/node-fetch-server'
import { router } from './app/router.ts'
let server = createServer(router)
let port = process.env.PORT || 3000
server.listen(port, () => {
console.log(`API server running on http://localhost:${port}`)
})
// Clean shutdown
process.on('SIGINT', () => {
server.close(() => process.exit(0))
})
process.on('SIGTERM', () => {
server.close(() => process.exit(0))
})
API Best Practices
Use proper HTTP status codes
200 OK - Successful GET, PATCH, PUT
201 Created - Successful POST that creates a resource
204 No Content - Successful DELETE
400 Bad Request - Invalid request data
404 Not Found - Resource doesn’t exist
500 Internal Server Error - Server error
interface ApiError {
error: string
message?: string
field?: string
}
return Response.json(
{ error: 'Validation failed', field: 'email', message: 'Invalid email format' },
{ status: 400 }
)
Version your API
export let apiRoutes = routes({
v1: {
users: {
index: 'GET /api/v1/users',
show: 'GET /api/v1/users/:id',
},
},
})