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

```typescript
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:

```bash
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.

## Middleware

To add route-specific middleware pass a `middleware` array when calling `registerApiRoute()`.

```typescript
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 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.

```typescript
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 Schemas

Zod schemas in the `openapi` configuration are converted to JSON Schema when the OpenAPI document is generated:

```typescript
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 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`.

```typescript
export const mastra = new Mastra({
  server: {
    build: {
      swaggerUI: true, // Enable in production builds
    },
    apiRoutes: [
      // Your routes...
    ],
  },
})
```

## 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`:

```typescript
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 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`

### Accessing user information

When a request is authenticated, the user object is available in the request context:

```typescript
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](https://mastra.ai/docs/server/auth).

## Continue generation after client disconnect

Built-in streaming helpers such as [`chatRoute()`](https://mastra.ai/reference/ai-sdk/chat-route) forward the incoming request's `AbortSignal` to `agent.stream()`. That's the right default when a browser disconnect should cancel the model call.

For custom streaming routes that should stop when the client disconnects, pass `c.req.raw.signal` to long-running work such as `agent.stream()`. Mastra's Node-based adapters also stop reading streamed `Response` bodies from custom routes when the client connection closes. Streamed response body errors that are not caused by client disconnects still propagate through the adapter's normal error handling. In Hono, disconnect behavior depends on the host runtime forwarding connection closes to `request.signal`.

```typescript
registerApiRoute('/stream', {
  method: 'GET',
  handler: async c => {
    const stream = await agent.stream(prompt, {
      abortSignal: c.req.raw.signal,
    })

    return stream.toTextStreamResponse()
  },
})
```

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.

```typescript
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 })
        },
      }),
    ],
  },
})
```

> **Note:** 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.

## Related

- [registerApiRoute() Reference](https://mastra.ai/reference/server/register-api-route): Full API reference
- [Server Middleware](https://mastra.ai/docs/server/middleware): Global middleware configuration
- [Mastra Server](https://mastra.ai/docs/server/mastra-server): Server configuration options