Skip to main content
1

Set up your router

First, create a router with middleware. The router handles all HTTP requests and applies middleware in order.
app/router.ts
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.
2

Define your routes

Create a type-safe routes definition using the routes helper. This provides autocomplete and type checking.
app/routes.ts
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.
3

Create a controller

Controllers group related route handlers together. Each action receives a request context with typed params.
app/users.ts
import type { Controller } from 'remix/fetch-router'
import { apiRoutes } from './routes.ts'

// Sample in-memory database
let users = [
  { id: 1, name: 'Alice', email: '[email protected]' },
  { id: 2, name: 'Bob', email: '[email protected]' },
]

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.
4

Map controllers to routes

Connect your controller to the router using the map method:
app/router.ts
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: [] })
})
5

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:
app/users.ts
import { validateJson } from './middleware/validate.ts'

export default {
  middleware: [validateJson()],
  actions: {
    // ... your actions
  },
} satisfies Controller<typeof apiRoutes.users>
6

Handle errors

Add error handling middleware to catch and format errors:
app/middleware/errors.ts
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:
app/router.ts
import { errorHandler } from './middleware/errors.ts'

let middleware = [
  errorHandler(),
  logger(),
  compression(),
]

export let router = createRouter({ middleware })
7

Start your server

Create a server to handle requests:
server.ts
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

Return consistent error formats

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',
    },
  },
})

Build docs developers (and LLMs) love