Custom API routes
By default, Mastra automatically exposes registered agents and workflows via its server. For additional behavior you can define your own HTTP routes.
Routes are provided with a helper registerApiRoute() from @mastra/core/server. Routes can live in the same file as the Mastra instance but separating them helps keep configuration concise.
import { Mastra } from '@mastra/core'
import { registerApiRoute } from '@mastra/core/server'
export const mastra = new Mastra({
server: {
apiRoutes: [
registerApiRoute('/my-custom-route', {
method: 'GET',
handler: async c => {
const mastra = c.get('mastra')
const agent = await mastra.getAgent('my-agent')
return c.json({ message: 'Custom route' })
},
}),
],
},
})
Once registered, a custom route will be accessible from the root of the server. For example:
curl http://localhost:4111/my-custom-route
Each route's handler receives the Hono Context. Within the handler you can access the Mastra instance to fetch or call agents and workflows.
MiddlewareDirect link to Middleware
To add route-specific middleware pass a middleware array when calling registerApiRoute().
import { Mastra } from '@mastra/core'
import { registerApiRoute } from '@mastra/core/server'
export const mastra = new Mastra({
server: {
apiRoutes: [
registerApiRoute('/my-custom-route', {
method: 'GET',
middleware: [
async (c, next) => {
console.log(`${c.req.method} ${c.req.url}`)
await next()
},
],
handler: async c => {
return c.json({ message: 'Custom route with middleware' })
},
}),
],
},
})
OpenAPI documentationDirect link to OpenAPI documentation
Custom routes can include OpenAPI metadata to appear in the Swagger UI alongside Mastra server routes. You can access the OpenAPI spec at /api/openapi.json, where both custom routes and built-in routes are listed. Pass an openapi option with standard OpenAPI operation fields.
import { Mastra } from '@mastra/core'
import { registerApiRoute } from '@mastra/core/server'
import { z } from 'zod'
export const mastra = new Mastra({
server: {
apiRoutes: [
registerApiRoute('/items/:itemId', {
method: 'GET',
openapi: {
summary: 'Get item by ID',
description: 'Retrieves a single item by its unique identifier',
tags: ['Items'],
parameters: [
{
name: 'itemId',
in: 'path',
required: true,
description: 'The item ID',
schema: { type: 'string' },
},
],
responses: {
200: {
description: 'Item found',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
},
},
},
},
404: {
description: 'Item not found',
},
},
},
handler: async c => {
const itemId = c.req.param('itemId')
return c.json({ id: itemId, name: 'Example Item' })
},
}),
],
},
})
Using Zod SchemasDirect link to Using Zod Schemas
Zod schemas in the openapi configuration are converted to JSON Schema when the OpenAPI document is generated:
import { Mastra } from '@mastra/core'
import { registerApiRoute } from '@mastra/core/server'
import { z } from 'zod'
const ItemSchema = z.object({
id: z.string(),
name: z.string(),
price: z.number(),
})
const CreateItemSchema = z.object({
name: z.string().min(1),
price: z.number().positive(),
})
export const mastra = new Mastra({
server: {
apiRoutes: [
registerApiRoute('/items', {
method: 'POST',
openapi: {
summary: 'Create a new item',
tags: ['Items'],
requestBody: {
required: true,
content: {
'application/json': {
schema: CreateItemSchema,
},
},
},
responses: {
201: {
description: 'Item created',
content: {
'application/json': {
schema: ItemSchema,
},
},
},
},
},
handler: async c => {
const body = await c.req.json()
return c.json({ id: 'new-id', ...body }, 201)
},
}),
],
},
})
Viewing in Swagger UIDirect link to Viewing in Swagger UI
When running in development mode (mastra dev) or with swaggerUI: true in build options, your custom routes appear in the Swagger UI at /swagger-ui.
export const mastra = new Mastra({
server: {
build: {
swaggerUI: true, // Enable in production builds
},
apiRoutes: [
// Your routes...
],
},
})
AuthenticationDirect link to Authentication
When authentication is configured on your Mastra server, custom API routes require authentication by default. To make a route publicly accessible, set requiresAuth: false:
import { Mastra } from '@mastra/core'
import { registerApiRoute } from '@mastra/core/server'
import { MastraJwtAuth } from '@mastra/auth'
export const mastra = new Mastra({
server: {
auth: new MastraJwtAuth({
secret: process.env.MASTRA_JWT_SECRET,
}),
apiRoutes: [
// Protected route (default behavior)
registerApiRoute('/protected-data', {
method: 'GET',
handler: async c => {
// Access authenticated user from request context
const user = c.get('requestContext').get('user')
return c.json({ message: 'Authenticated user', user })
},
}),
// Public route (no authentication required)
registerApiRoute('/webhooks/github', {
method: 'POST',
requiresAuth: false, // Explicitly opt out of authentication
handler: async c => {
const payload = await c.req.json()
// Process webhook without authentication
return c.json({ received: true })
},
}),
],
},
})
Authentication behaviorDirect link to Authentication behavior
- No auth configured: All routes (built-in and custom) are public
- Auth configured:
- Mastra-provided routes (
/api/agents/*,/api/workflows/*, etc.) require authentication - Custom routes require authentication by default
- Custom routes can opt out with
requiresAuth: false
- Mastra-provided routes (
Accessing user informationDirect link to Accessing user information
When a request is authenticated, the user object is available in the request context:
registerApiRoute('/user-profile', {
method: 'GET',
handler: async c => {
const requestContext = c.get('requestContext')
const user = requestContext.get('user')
return c.json({ user })
},
})
For more information about authentication providers, see the Auth documentation.
Continue generation after client disconnectDirect link to Continue generation after client disconnect
Built-in streaming helpers such as chatRoute() forward the incoming request's AbortSignal to agent.stream(). That is the right default when a browser disconnect should cancel the model call.
If you want the server to keep generating and persist the final response even after the client disconnects, build a custom route around the underlying MastraModelOutput. Start the agent stream without forwarding c.req.raw.signal, then call consumeStream() in the background so generation continues server-side.
import {
createUIMessageStream,
createUIMessageStreamResponse,
InferUIMessageChunk,
UIMessage,
} from 'ai'
import { toAISdkStream } from '@mastra/ai-sdk'
import { Mastra } from '@mastra/core'
import { registerApiRoute } from '@mastra/core/server'
export const mastra = new Mastra({
server: {
apiRoutes: [
registerApiRoute('/chat/persist/:agentId', {
method: 'POST',
handler: async c => {
const { messages, memory } = await c.req.json()
const mastra = c.get('mastra')
const agent = mastra.getAgent(c.req.param('agentId'))
const stream = await agent.stream(messages, {
memory,
// Do not pass c.req.raw.signal if this route should keep running
// after the client disconnects.
})
void stream.consumeStream().catch(error => {
mastra.getLogger()?.error('Background stream consumption failed', { error })
})
const uiStream = createUIMessageStream({
originalMessages: messages,
execute: async ({ writer }) => {
for await (const part of toAISdkStream(stream, { from: 'agent' })) {
writer.write(part as InferUIMessageChunk<UIMessage>)
}
},
})
return createUIMessageStreamResponse({ stream: uiStream })
},
}),
],
},
})
Use this pattern only when you intentionally want work to continue after the HTTP client is gone. If you want disconnects to cancel generation, keep using chatRoute() or forward the request AbortSignal yourself.
RelatedDirect link to Related
- registerApiRoute() Reference - Full API reference
- Server Middleware - Global middleware configuration
- Mastra Server - Server configuration options