Motia Icon
Development Guide

Middleware

Run code before and after your API handlers

What is Middleware?

Middleware runs before your API handler. Use it for authentication, logging, error handling, or any logic that applies to multiple endpoints.


How It Works

A middleware is a function that receives three arguments:

middleware(req, ctx, next)
  • req - The incoming request (same as handler)
  • ctx - The context object (same as handler)
  • next() - Call this to continue to the handler

If you don't call next(), the request stops. The handler never runs.


Simple Example

import { ApiMiddleware } from 'motia'
 
const authMiddleware: ApiMiddleware = async (req, ctx, next) => {
  if (!req.headers.authorization) {
    return { status: 401, body: { error: 'Unauthorized' } }
  }
  return next()
}
 
export const config = {
  name: 'ProtectedEndpoint',
  type: 'api',
  path: '/protected',
  method: 'GET',
  middleware: [authMiddleware]
}
 
export const handler = async (req, ctx) => {
  return { status: 200, body: { message: 'Success' } }
}

Execution Order

Middleware runs in the order you list them:

export const config = {
  name: 'MyEndpoint',
  type: 'api',
  path: '/endpoint',
  method: 'POST',
  middleware: [
    loggingMiddleware,  // Runs first
    authMiddleware,     // Runs second  
    errorMiddleware     // Runs third
  ]
}

Modifying Responses

Await next() to get the response, then modify it:

const addHeadersMiddleware = async (req, ctx, next) => {
  const response = await next()
  
  return {
    ...response,
    headers: {
      ...response.headers,
      'X-Request-Id': ctx.traceId
    }
  }
}

Passing Data to Handlers

Middleware can attach data to the req object, making it available to your handler. This is perfect for authentication—verify the user once in middleware, then use their details in the handler.

First, extend the request type in api.d.ts:

// api.d.ts
import 'motia'
 
declare module 'motia' {
  interface ApiRequest {
    user?: { id: string; role: string }
  }
}

Then attach the data in your middleware:

const authMiddleware: ApiMiddleware = async (req, ctx, next) => {
  // Verify token...
  req.user = { id: '123', role: 'admin' }
  return next()
}

Now use it in your handler:

export const handler = async (req, ctx) => {
  // req.user is typed and ready to use
  if (req.user?.role === 'admin') {
    return { status: 200, body: { message: 'Welcome Admin' } }
  }
  return { status: 403, body: { error: 'Forbidden' } }
}

Learn more: Check out the Middleware Auth Handler Example to see a complete project with JWT validation and type safety.


Error Handling

Catch errors from handlers:

import { ZodError } from 'zod'
 
const errorMiddleware = async (req, ctx, next) => {
  try {
    return await next()
  } catch (error: any) {
    if (error instanceof ZodError) {
      ctx.logger.error('Validation error', { errors: error.errors })
      return { status: 400, body: { error: 'Validation failed' } }
    }
 
    ctx.logger.error('Unexpected error', { error: error.message })
    return { status: 500, body: { error: 'Internal server error' } }
  }
}

Reusing Middleware

Create middleware files in a shared location:

middlewares/core.middleware.ts
export const coreMiddleware = async (req, ctx, next) => {
  try {
    return await next()
  } catch (error) {
    ctx.logger.error('Error', { error })
    return { status: 500, body: { error: 'Internal server error' } }
  }
}

Import and use across steps:

steps/user.step.ts
import { coreMiddleware } from '../middlewares/core.middleware'
 
export const config = {
  name: 'GetUser',
  type: 'api',
  path: '/users/:id',
  method: 'GET',
  middleware: [coreMiddleware]
}

What's Next?

Need help? See our Community Resources for questions, examples, and discussions.

On this page