Skip to main content

Middleware

Mastra servers can execute custom middleware functions before or after an API route handler is invoked. This is useful for things like authentication, logging, injecting request-specific context or adding CORS headers.

A middleware receives the Hono Context (c) and a next function. If it returns a Response the request is short-circuited. Calling next() continues processing the next middleware or route handler.

import { Mastra } from '@mastra/core'

export const mastra = new Mastra({
server: {
middleware: [
{
handler: async (c, next) => {
// Example: Add authentication check
const authHeader = c.req.header('Authorization')
if (!authHeader) {
return new Response('Unauthorized', { status: 401 })
}

await next()
},
path: '/api/*',
},
// Add a global request logger
async (c, next) => {
console.log(`${c.req.method} ${c.req.url}`)
await next()
},
],
},
})

To attach middleware to a single route pass the middleware option to registerApiRoute:

registerApiRoute('/my-custom-route', {
method: 'GET',
middleware: [
async (c, next) => {
console.log(`${c.req.method} ${c.req.url}`)
await next()
},
],
handler: async c => {
const mastra = c.get('mastra')
return c.json({ message: 'Hello, world!' })
},
})

Common examples
Direct link to Common examples

Using RequestContext
Direct link to using-requestcontext

You can populate RequestContext dynamically in server middleware by extracting information from the request. In this example, the temperature-unit is set based on the Cloudflare CF-IPCountry header to ensure responses match the user's locale.

src/mastra/index.ts
import { Mastra } from '@mastra/core'
import { RequestContext } from '@mastra/core/request-context'
import { testWeatherAgent } from './agents/test-weather-agent'

export const mastra = new Mastra({
agents: { testWeatherAgent },
server: {
middleware: [
async (context, next) => {
const country = context.req.header('CF-IPCountry')
const requestContext = context.get('requestContext')

requestContext.set('temperature-unit', country === 'US' ? 'fahrenheit' : 'celsius')

await next()
},
],
},
})

Authentication
Direct link to Authentication

{
handler: async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 });
}

// Validate token here
await next();
},
path: '/api/*',
}

Authorization (User Isolation)
Direct link to Authorization (User Isolation)

Authentication verifies who the user is. Authorization controls what they can access. Without resource ID scoping, an authenticated user could access other users' threads by guessing IDs or manipulating the resourceId parameter.

The simplest way to scope memory and threads to the authenticated user is the mapUserToResourceId callback in the auth config:

import { Mastra } from '@mastra/core'

export const mastra = new Mastra({
server: {
auth: {
authenticateToken: async token => {
return verifyToken(token) // { id: 'user-123', orgId: 'org-456', ... }
},
mapUserToResourceId: user => user.id,
},
},
})

After successful authentication, mapUserToResourceId is called with the authenticated user object. The returned value is set as MASTRA_RESOURCE_ID_KEY on the request context, which works across all server adapters (Hono, Express, Next.js, etc.).

The resource ID doesn't have to be user.id. Common patterns:

// Org-scoped
mapUserToResourceId: user => `${user.orgId}:${user.id}`

// From a JWT claim
mapUserToResourceId: user => user.tenantId

// Composite key
mapUserToResourceId: user => `${user.workspaceId}:${user.projectId}:${user.id}`

With a resource ID set, the server automatically:

  • Filters thread listing to only return threads owned by the user
  • Validates thread access and returns 403 if accessing another user's thread
  • Forces thread creation to use the authenticated user's ID
  • Validates message operations including deletion, ensuring messages belong to owned threads

Even if a client passes ?resourceId=other-user-id, the auth-set value takes precedence. Attempts to access threads or messages owned by other users will return a 403 error.

Advanced: Setting resource ID in middleware
Direct link to Advanced: Setting resource ID in middleware

For more complex scenarios (e.g., looking up the resource ID from a database), you can set MASTRA_RESOURCE_ID_KEY directly in middleware:

import { Mastra } from '@mastra/core'
import { MASTRA_RESOURCE_ID_KEY } from '@mastra/core/request-context'
import { getAuthenticatedUser } from '@mastra/server/auth'

export const mastra = new Mastra({
server: {
auth: {
authenticateToken: async token => verifyToken(token),
},
middleware: [
{
path: '/api/*',
handler: async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}

const user = await getAuthenticatedUser<{ id: string }>({
mastra: c.get('mastra'),
token,
request: c.req.raw,
})
const requestContext = c.get('requestContext')

if (!user) {
return c.json({ error: 'Unauthorized' }, 401)
}

requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id)
return next()
},
},
],
},
})

server.middleware runs before Mastra's per-route auth checks. When middleware needs the authenticated user, call getAuthenticatedUser() to resolve it from the configured auth provider without changing the default route auth flow.

Using MASTRA_THREAD_ID_KEY
Direct link to using-mastra_thread_id_key

You can also set MASTRA_THREAD_ID_KEY to override the client-provided thread ID:

import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY } from '@mastra/core/request-context'

// Force operations to use a specific thread
requestContext.set(MASTRA_THREAD_ID_KEY, validatedThreadId)

This is useful when you want to restrict operations to a specific thread that you've validated through other means.

CORS support
Direct link to CORS support

{
handler: async (c, next) => {
c.header('Access-Control-Allow-Origin', '*');
c.header(
'Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, OPTIONS',
);
c.header(
'Access-Control-Allow-Headers',
'Content-Type, Authorization',
);

if (c.req.method === 'OPTIONS') {
return new Response(null, { status: 204 });
}

await next();
},
}

Request logging
Direct link to Request logging

{
handler: async (c, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(`${c.req.method} ${c.req.url} - ${duration}ms`);
},
}

Related