- name
- stacks-api
- description
- Use when building, modifying, or debugging API endpoints in a Stacks application - defining routes, handling requests, API middleware, working with the API server, HTTP client (fetcher), API resources, or OpenAPI generation. Covers both @stacksjs/api utilities and the stacks-api server implementation.
- license
- MIT
- compatibility
- Bun >= 1.3.0, TypeScript
- allowed-tools
- Read Edit Write Bash Grep Glob
# Stacks API
The Stacks API system comprises two packages: the `@stacksjs/api` utilities package (HTTP client, API resources, OpenAPI generation) and the `stacks-api` server (Bun HTTP server with hot-reload). Routing is delegated to `@stacksjs/router` which wraps `@stacksjs/bun-router`.
## Key Paths
- Core utilities (package): `storage/framework/core/api/src/`
- API server: `storage/framework/api/`
- Router package: `storage/framework/core/router/src/`
- Route definitions: `routes/`
- Route registry: `app/Routes.ts` (re-exports `storage/framework/defaults/app/Routes.ts`)
- Port configuration: `config/ports.ts`
- Generated OpenAPI spec: `storage/framework/api/openapi.json`
- Generated API types: `storage/framework/api/api-types.ts`
- Actions directory: `app/Actions/`
- Controllers directory: `app/Controllers/`
- Middleware directory: `app/Middleware/`
- Middleware alias map: `app/Middleware.ts`
- Package: `@stacksjs/api`
## Source Files
```
core/api/src/
├── index.ts # Re-exports fetcher, generate-openapi, resource
├── fetcher.ts # Fetcher HTTP client class (fluent API)
├── generate-openapi.ts # OpenAPI 3.0 spec generator from route definitions
├── resource.ts # JsonResource, ResourceCollection, PaginatedResourceCollection
└── types.ts # FetcherResponse, QueryParams, BodyData, FileAttachment
framework/api/
├── dev.ts # API server entry point (Bun --hot, file watchers)
├── build.ts # Bun.build config for production
├── api-types.ts # Auto-generated TypeScript types from OpenAPI spec
├── openapi.json # Generated OpenAPI 3.0 specification
├── package.json # Dependencies: @stacksjs/router, queue, storage, logging
└── tsconfig.json
core/router/src/
├── index.ts # Re-exports bun-router + stacks-specific additions
├── stacks-router.ts # Route object, serve(), url(), action/controller resolution
├── response.ts # Response factory (json, text, html, redirect, etc.)
├── middleware.ts # Middleware class definition
├── request-context.ts # AsyncLocalStorage-based request context (request proxy)
├── route-loader.ts # Loads route files from registry with auto-prefixing
├── error-handler.ts # Ignition-style error pages, query tracking
└── action-paths.ts # StacksActionPath type definitions
routes/
├── api.ts # Main API routes (auto-prefixed with /api; see #1835)
├── v1.ts # Versioned routes (loaded at /v1/*)
├── buddy.ts # Buddy CLI routes (loaded at /buddy/*)
└── users.ts # Example user routes (commented out)
```
## API Server (dev.ts)
The API server runs on Bun's HTTP server with hot-reload (`bun --hot run dev.ts`).
```typescript
import { serve } from '@stacksjs/router'
serve({
port: ports.api || 3008, // Configurable via config/ports.ts or PORT_API env
timezone: app.timezone || 'UTC',
})
```
### Hot Reload Watchers
The dev server watches these directories for changes and auto-reloads:
- `storage/framework/core/*/src/` -- rebuilds core packages on change
- `routes/` -- detects route file changes
- `app/Actions/` -- invalidates module cache, hot-reloads actions
- `app/Controllers/` -- invalidates module cache, hot-reloads controllers
- `app/Middleware/` -- invalidates module cache, hot-reloads middleware
- `app/Models/` (user models) -- regenerates the model artifacts (there is no
`generate:model-files` command; the dev server does this itself, and
`buddy generate:types` is the manual equivalent)
Production build: `bun build.ts` bundles `dev.ts` to `dist/dev.js`.
## Route Definitions
### Route Registry (app/Routes.ts)
Routes are organized in `routes/*.ts` files and registered in `app/Routes.ts`:
```typescript
export interface RouteDefinition {
path: string // Route file path relative to routes/
prefix?: string // URL prefix (overrides key-based prefix)
middleware?: string | string[] // Middleware for all routes in file
}
export type RouteRegistry = Record<string, string | RouteDefinition>
export default {
'api': 'api', // Auto-prefixed with /api (see #1835)
'v1': { path: 'v1', prefix: 'v1' }, // routes at /v1/*
// 'admin': { path: 'admin', middleware: ['auth'] },
} satisfies RouteRegistry
```
`'web'` is the only key that loads at root `/` with no prefix; all other keys (including `'api'`) auto-prefix with `/<key>`. The `'api'` auto-prefix lines up with the rpx proxy forward path so a user route registered as `route.get('/cart/add', ...)` in `routes/api.ts` resolves at `https://<domain>/api/cart/add` through the dev proxy.
### Route Methods
```typescript
import { route, response } from '@stacksjs/router'
// Inline handler
route.get('/hello', () => response.json({ message: 'Hello' }))
route.post('/users', () => response.created({ id: 1 }))
// String-based handler (Action)
route.get('/subscribers', 'Actions/SubscriberIndexOrmAction')
route.post('/login', 'Actions/Auth/LoginAction')
// String-based handler (Controller@method)
route.get('/coming-soon', 'Controllers/ComingSoonController@index')
route.get('/stats', 'Controllers/QueryController@getStats')
// Route with path parameters
route.get('/foo/bar/{id}', () => response.text('hello'))
route.get('/users/{id}/posts/{postId}', handler)
// Route chaining: middleware + naming
route.get('/tokens', 'Actions/Auth/ListTokensAction').middleware('auth')
route.post('/email/subscribe', 'Actions/SubscriberEmailAction').name('email.subscribe')
route.delete('/tokens/{id}', 'Actions/Auth/RevokeTokenAction').middleware('auth').name('token.revoke')
// Route groups (prefix + middleware)
route.group({ prefix: '/auth' }, () => {
route.post('/refresh', 'Actions/Auth/RefreshTokenAction')
route.get('/tokens', 'Actions/Auth/ListTokensAction').middleware('auth')
})
route.group({ middleware: 'auth' }, () => {
route.get('/me', 'Actions/Auth/AuthUserAction')
route.post('/logout', 'Actions/Auth/LogoutAction')
})
// Health check (adds GET /health)
route.health()
```
### Named Routes & URL Generation
```typescript
import { url } from '@stacksjs/router'
// Define named routes
route.get('/api/email/unsubscribe', 'Actions/UnsubscribeAction').name('email.unsubscribe')
route.get('/users/{id}/posts/{postId}', handler).name('user.post')
// Generate URLs
url('email.unsubscribe', { token: 'abc-123' })
// => https://stacksjs.com/api/email/unsubscribe?token=abc-123
url('user.post', { id: 42, postId: 7 })
// => https://stacksjs.com/users/42/posts/7
```
Path parameters (`{id}`) are substituted; remaining params become query string. Uses `APP_URL` env var as base (defaults to `https://localhost`).
## Action Resolution
String-based route handlers are resolved at runtime:
1. **Actions** (`'Actions/MyAction'`): checks `app/Actions/MyAction.ts` first, falls back to `storage/framework/defaults/app/Actions/MyAction.ts`
2. **ORM Actions** (`'Actions/UserIndexOrmAction'`): loaded from `storage/framework/actions/src/`
3. **Controllers** (`'Controllers/MyController@method'`): checks `app/Controllers/` first, falls back to defaults
Action modules must export a default object with a `handle(req: EnhancedRequest)` method:
```typescript
// app/Actions/MyAction.ts
export default {
handle(request: EnhancedRequest): Response {
const name = request.get('name', 'world')
return Response.json({ message: `Hello ${name}` })
}
}
```
Actions can also define `validations` for automatic input validation:
```typescript
export default {
validations: {
email: {
rule: { validate: (value) => ({ valid: typeof value === 'string' && value.includes('@') }) },
message: 'A valid email is required',
},
},
handle(request: EnhancedRequest): Response {
// Request is validated before reaching handle()
return Response.json({ success: true })
},
}
```
Validation failures return a `422` response:
```json
{ "error": "Validation failed", "errors": { "email": ["A valid email is required"] } }
```
## Response Factory
```typescript
import { response } from '@stacksjs/router'
// JSON (200)
response.json({ message: 'OK' })
response.json(data, { status: 200, headers: { 'X-Custom': 'value' }, pretty: true })
// Status shortcuts
response.created({ id: 1 }) // 201
response.noContent() // 204
response.badRequest({ error: 'Bad' }) // 400
response.unauthorized() // 401 (default: { error: 'Unauthorized' })
response.forbidden() // 403 (default: { error: 'Forbidden' })
response.notFound() // 404 (default: { error: 'Not Found' })
response.error() // 500 (default: { error: 'Internal Server Error' })
// Other content types
response.text('Hello', { status: 200 })
response.html('<h1>Hello</h1>')
response.redirect('/new-url', 302) // 301 | 302 | 303 | 307 | 308
```
All methods return a standard `Response` object.
## Enhanced Request (EnhancedRequest)
Route handlers receive an `EnhancedRequest` (extends `Request`) with Laravel-style helper methods:
```typescript
// Input access (merges query params + JSON body + form body + route params)
request.get<T>(key: string, defaultValue?: T): T
request.input<T>(key: string, defaultValue?: T): T
request.all(): Record<string, unknown>
request.only<T>(keys: string[]): T
request.except<T>(keys: string[]): T
// Presence checks
request.has(key: string | string[]): boolean
request.hasAny(keys: string[]): boolean
request.filled(key: string | string[]): boolean
request.missing(key: string | string[]): boolean
// Typed getters
request.string(key: string, defaultValue?: string): string
request.integer(key: string, defaultValue?: number): number
request.float(key: string, defaultValue?: number): number
request.boolean(key: string, defaultValue?: boolean): boolean
request.array<T>(key: string): T[]
// File handling (returns UploadedFile with store/storeAs methods)
request.file(key: string): UploadedFile | null
request.getFiles(key: string): UploadedFile[]
request.hasFile(key: string): boolean
request.allFiles(): Record<string, UploadedFile | UploadedFile[]>
// Authentication (set by auth middleware)
await request.user(): Promise<any>
await request.userToken(): Promise<any>
await request.tokenCan(ability: string): Promise<boolean>
await request.tokenCant(ability: string): Promise<boolean>
request.bearerToken(): string | null
```
## Request Context (AsyncLocalStorage)
Access the current request from anywhere using the `request` proxy:
```typescript
import { request, getCurrentRequest, runWithRequest, setCurrentRequest } from '@stacksjs/router'
// The `request` export is a Proxy that reads from AsyncLocalStorage
const token = request.bearerToken()
const user = await request.user()
// Manually run code in a request context
runWithRequest(enhancedReq, () => {
// request proxy works here
})
```
## Middleware
### Defining Middleware
```typescript
// app/Middleware/Auth.ts
import { Middleware } from '@stacksjs/router'
export default new Middleware({
name: 'Auth',
priority: 1, // Lower numbers run first (default: 10)
async handle(request) {
const token = request.bearerToken()
if (!token) {
const error = new Error('Unauthorized') as Error & { statusCode: number }
error.statusCode = 401
throw error
}
// Set authenticated user on request
;(request as any)._authenticatedUser = user
},
})
```
### Middleware Alias Map
View on GitHub