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-exportsstorage/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).
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 changeroutes/-- detects route file changesapp/Actions/-- invalidates module cache, hot-reloads actionsapp/Controllers/-- invalidates module cache, hot-reloads controllersapp/Middleware/-- invalidates module cache, hot-reloads middlewareapp/Models/(user models) -- triggersbuddy generate:model-files
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:
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
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
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:
- Actions (
'Actions/MyAction'): checksapp/Actions/MyAction.tsfirst, falls back tostorage/framework/defaults/app/Actions/MyAction.ts - ORM Actions (
'Actions/UserIndexOrmAction'): loaded fromstorage/framework/actions/src/ - Controllers (
'Controllers/MyController@method'): checksapp/Controllers/first, falls back to defaults
Action modules must export a default object with a handle(req: EnhancedRequest) method:
// 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:
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:
{ "error": "Validation failed", "errors": { "email": ["A valid email is required"] } }
Response Factory
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:
// 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:
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
// 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
Define shorthand names in app/Middleware.ts:
export default {
auth: 'Auth', // maps to app/Middleware/Auth.ts
verified: 'EnsureEmailIsVerified',
abilities: 'CheckAbilities', // supports params: 'abilities:read,write'
}
Middleware Resolution Order
- Parse alias via
app/Middleware.ts(e.g.,'auth'->'Auth') - Load from
app/Middleware/Auth.ts(user override) - Fall back to
storage/framework/defaults/app/Middleware/Auth.ts
Middleware with Parameters
route.get('/admin', handler).middleware('abilities:admin,write')
// Params accessible in middleware via: (request as any)._middlewareParams.abilities === 'admin,write'
Which client to reach for
Stacks has three, and they answer different questions. Pick by who is calling.
| Caller | Client | Why |
|---|---|---|
| TypeScript in this repo or a workspace package | Typed client (below) | Full input/output inference with no generation step. Change a route, the call site stops compiling. |
| Anything that is not TypeScript here - native iOS/Android via the Craft bridge, third-party integrators, Swagger UI | Generated REST client (buddy generate:openapi) |
Needs a real spec. That pipeline is unchanged and permanent. |
| One-off calls to any URL | Fetcher (below) |
No route awareness, and none intended. |
The first two are both permanent. This is not a migration off REST.
Typed client (zero generation)
Register routes with createTypedRouter() and the compiler can see both ends —
no buddy generate:openapi, no committed file to go stale.
// routes/api.ts
import IndexAction from '../app/Actions/Project/IndexAction'
import StoreAction from '../app/Actions/Project/StoreAction'
import { createTypedRouter } from '@stacksjs/router'
export const api = createTypedRouter()
.get('/v1/projects', IndexAction)
.get('/v1/projects/{id}', ShowAction)
.post('/v1/projects', StoreAction, { middleware: 'auth' })
export type AppRoutes = typeof api
// any TypeScript consumer
import type { AppRoutes } from '../routes/api'
import { createTypedClient } from '@stacksjs/router'
const client = createTypedClient<AppRoutes>({ baseUrl: 'https://api.example.com' })
const projects = await client.get('/v1/projects')
const client.get('/v1/projects/{id}', { params: { id: '42' } })
const created = await client.post('/v1/projects', { name: 'apollo', budget: 1200 })
An unknown path is a compile error. A body that does not match the action's
validations is a compile error. The result is the action's own return type.
How it infers. Input comes from the action's validations — the same object
the validator runs, so the two cannot drift. Output comes from handle's return
type. An action returning a Response or a stream is typed unknown, honestly:
it took over the wire format.
Per-route settings are an argument, not a chained call — chaining
.middleware() would return the route and lose the accumulated type:
.post('/v1/projects', StoreAction, {
middleware: ['auth', 'can:create,project'],
name: 'projects.store',
skipCsrf: true,
rateLimit: { max: 10, window: 'minute' },
})
No .group(). A prefix applied only at runtime makes every accumulated path
type wrong; one applied only in the type is a second place for the URL to live.
Write the full path.
The string form is untouched. route.get('/x', 'Actions/Foo') stays exactly
as it is — lazy import, hot-reload friendly — and simply produces no inferred
types. A route wanting inference opts in by using the builder instead. Routes
registered either way still land in the generated OpenAPI document.
Generated CRUD. A useApi model's endpoints are describable too, without
touching them:
import type { ApiRoutesFor } from '@stacksjs/orm'
import type Product from '../app/Models/Product'
type AppRoutes = RoutesOf<typeof api> & ApiRoutesFor<typeof Product>
await client.get('/api/products') // typed listing envelope
The builder, the client and the route-map contract all live in
@stacksjs/bun-router and are re-exported by @stacksjs/router. Import
createTypedClient from @stacksjs/bun-router directly in a browser bundle.
Fetcher (HTTP Client)
A fluent HTTP client for ad hoc requests, with no route awareness:
import { fetcher } from '@stacksjs/api'
// GET request
const res = await fetcher.get<UserResponse>('/api/users')
res.data // UserResponse
res.status // 200
res.isOk // true
res.ok() // true (status === 200)
res.notFound() // false
// POST with JSON body
const res = await fetcher
.withBody({ name: 'John', email: 'john@example.com' })
.post<User>('/api/users')
// PUT/PATCH/DELETE
await fetcher.withBody(data).put<User>('/api/users/1')
await fetcher.withBody(data).patch<User>('/api/users/1')
await fetcher.delete('/api/users/1')
// Query parameters
await fetcher.withQueryParams({ page: 1, limit: 25 }).get('/api/users')
// Custom headers
await fetcher.withHeaders({ 'X-Custom': 'value' }).get('/api/users')
// Authentication
await fetcher.withToken('bearer-token-here').get('/api/me')
await fetcher.withBasicAuth('user', 'pass').get('/api/protected')
await fetcher.withDigestAuth('user', 'pass').get('/api/digest-protected')
// Form data
await fetcher.asForm().withBody({ email: 'test@test.com' }).post('/subscribe')
// File uploads (multipart/form-data)
await fetcher
.attach('avatar', file, 'photo.jpg')
.withBody({ name: 'John' })
.post('/api/upload')
// Accept header
await fetcher.acceptJson().get('/api/data')
await fetcher.accept('text/xml').get('/api/feed')
FetcherResponse
interface FetcherResponse<T = any> {
data: T
status: number
headers: Headers
isOk: boolean
// Status checkers
ok(): boolean // 200
created(): boolean // 201
accepted(): boolean // 202
noContent(): boolean // 204
movedPermanently(): boolean // 301
found(): boolean // 302
badRequest(): boolean // 400
unauthorized(): boolean // 401
paymentRequired(): boolean // 402
forbidden(): boolean // 403
notFound(): boolean // 404
requestTimeout(): boolean // 408
conflict(): boolean // 409
unprocessableEntity(): boolean // 422
tooManyRequests(): boolean // 429
serverError(): boolean // 500
}
Fetcher Types
type QueryParams = Record<string, string | number | boolean | null | undefined>
type BodyData = Record<string, any>
interface FileAttachment {
name: string
content: Blob
filename?: string
headers?: Record<string, string>
}
API Resources (Laravel-style)
JsonResource
Transform models into JSON responses with conditional field inclusion:
import { JsonResource } from '@stacksjs/api'
class UserResource extends JsonResource<User> {
toArray() {
return {
id: this.resource.id,
name: this.resource.name,
email: this.resource.email,
// Conditional inclusion
secret: this.when(isAdmin, this.resource.secret),
avatar: this.whenNotNull(this.resource.avatar),
// Relationship loading
posts: this.whenLoaded('posts', () => PostResource.collection(this.resource.posts)),
posts_count: this.whenCounted('posts'),
// Conditional merge
...this.mergeWhen(isAdmin, { admin_notes: this.resource.adminNotes }),
}
}
}
// Single resource
const response = new UserResource(user).toResponse()
// => { data: { id: 1, name: 'John', email: 'john@...' } }
// With additional data
new UserResource(user).withAdditional({ meta: { version: 1 } }).toResponse()
// JSON string
new UserResource(user).toJson()
JsonResource Methods
abstract class JsonResource<T> {
resource: T
additional: Record<string, any>
static wrap: string | null = 'data' // Wrap key, null to disable
abstract toArray(request?: Request): Record<string, any>
// Instance methods
withRequest(request: Request): this
withAdditional(data: Record<string, any>): this
resolve(request?: Request): Record<string, any>
toResponse(request?: Request): Record<string, any>
toJson(request?: Request): string
// Static methods
static collection<T, R>(resources: T[]): ResourceCollection<T, R>
static withoutWrapping(): void // Set wrap = null for this class
static wrapWith(key: string): void // Set custom wrap key
// Conditional helpers (protected, use inside toArray)
protected when<V>(condition: boolean | (() => boolean), value: V | (() => V), defaultValue?: any): V | MissingValue
protected whenNotNull<V>(value: V | null | undefined, transform?: (v: V) => any): any | MissingValue
protected whenLoaded<V>(relationship: string, value?: V | (() => V), defaultValue?: any): V | any[] | MissingValue
protected whenCounted(relationship: string, defaultValue?: number): number | MissingValue
protected merge(data: Record<string, any>): MergeValue
protected mergeWhen(condition: boolean | (() => boolean), data: Record<string, any> | (() => Record<string, any>)): MergeValue | MissingValue
}
ResourceCollection
class ResourceCollection<T, R extends JsonResource<T>> {
resources: T[]
static wrap: string | null = 'data'
withRequest(request: Request): this
withAdditional(data: Record<string, any>): this
resolve(request?: Request): Record<string, any>[]
toResponse(request?: Request): Record<string, any>
toJson(request?: Request): string
count(): number
isEmpty(): boolean
isNotEmpty(): boolean
}
// Usage
const collection = UserResource.collection(users)
collection.toResponse()
// => { data: [{ id: 1, ... }, { id: 2, ... }] }
PaginatedResourceCollection
class PaginatedResourceCollection<T, R extends JsonResource<T>> extends ResourceCollection<T, R> {
meta: PaginationMeta
links: PaginationLinks
// Create from raw pagination data
static fromPagination<T, R>(
data: T[],
ResourceClass: new (resource: T) => R,
options: { currentPage: number, perPage: number, total: number, baseUrl?: string },
): PaginatedResourceCollection<T, R>
toResponse(): Record<string, any>
// => { data: [...], meta: { current_page, from, last_page, per_page, to, total }, links: { first, last, prev, next } }
}
interface PaginationMeta {
current_page: number
from: number | null
last_page: number
per_page: number
to: number | null
total: number
}
interface PaginationLinks {
first: string | null
last: string | null
prev: string | null
next: string | null
}
Anonymous Resources
Quick inline transformations without defining a class:
import { resource, collection } from '@stacksjs/api'
// Single resource
resource(user, (u) => ({ id: u.id, name: u.name })).toResponse()
// => { data: { id: 1, name: 'John' } }
// Collection
collection(users, (u) => ({ id: u.id, name: u.name })).toResponse()
// => { data: [{ id: 1, name: 'John' }, ...] }
Conditional Value Types
class MissingValue {
static instance: MissingValue
isMissing(): boolean
}
class MergeValue {
constructor(data: Record<string, any>)
}
class ConditionalValue {
constructor(condition: boolean | (() => boolean), value: any, defaultValue?: any)
resolve(): any
}
MissingValue entries are stripped from the final output by filterAndResolve() (max depth: 20).
OpenAPI Generation
import { generateOpenApi } from '@stacksjs/api'
await generateOpenApi()
// Reads all registered routes via `route.routes()`
// Writes OpenAPI 3.0 spec to storage/framework/api/openapi.json
The generated spec includes:
- Path definitions from route URLs
- HTTP methods from route registrations
- Path parameters extracted from
{param}placeholders - Response/request schemas if defined on routes (
route.responseSchema,route.requestSchema) - Operation IDs from route callbacks (action paths)
TypeScript types can be generated from the spec:
# Defined in package.json scripts
bun run generate-types
# Runs: open-api ./../../api/openapi.json --output ./../../api/api-types.ts
Error Handling
Error Response Structure
interface ErrorResponseBody {
error: string
message: string
status: number
timestamp: string
details?: Record<string, unknown>
}
Development vs Production
- Development: Ignition-style HTML error pages with full stack traces, database queries, and request context
- Production: Simple JSON or HTML error responses (no stack traces)
- Detection:
APP_ENV !== 'production' && NODE_ENV !== 'production' - API requests (Accept:
application/json): always get JSON error responses
Error Functions
import {
createErrorResponse,
createMiddlewareErrorResponse,
createValidationErrorResponse,
createNotFoundResponse,
trackQuery,
clearTrackedQueries,
} from '@stacksjs/router'
// Track queries for error context (circular buffer, max 50)
trackQuery('SELECT * FROM users', 12.5, 'sqlite')
// Create error responses
await createErrorResponse(error, request, { status: 500, handlerPath: 'Actions/MyAction' })
await createMiddlewareErrorResponse(error, request) // Uses error.statusCode
createValidationErrorResponse({ email: ['Required'] }, request) // 422
await createNotFoundResponse('/missing-path', request) // 404
Sensitive Data Sanitization
Error pages automatically redact fields matching these patterns: password, secret, token, api_key, authorization, credential, cookie, session_id, and others.
Port Configuration (config/ports.ts)
{
frontend: env.PORT ?? 3000,
backend: env.PORT_BACKEND ?? 3001,
admin: env.PORT_ADMIN ?? 3002,
api: env.PORT_API ?? 3008,
// ... other services
}
Route Groups in routes/api.ts
The main routes/api.ts file defines route groups for major feature areas:
| Prefix | Feature | Actions Prefix |
|---|---|---|
/auth |
Authentication & tokens | Actions/Auth/ |
/password |
Password reset flow | Actions/Password/ |
/payments |
Stripe payment integration | Actions/Payment/ |
/commerce |
E-commerce (products, orders, customers, coupons, gift cards, tax rates, reviews, receipts, POS, waitlists) | Actions/Commerce/ |
/shipping |
Shipping methods, rates, zones, delivery routes, drivers, digital delivery, license keys | Actions/Commerce/Shipping/ |
/cms |
Blog posts, authors, categories, tags, comments, pages | Actions/Cms/ |
/blog |
Public blog endpoints (read-only) | Actions/Cms/ |
/monitoring |
Error tracking & resolution | Actions/Monitoring/ |
/dashboard |
Dashboard stats & health | Actions/Dashboard/ |
/queries |
Query log dashboard | Controllers/QueryController@ |
/queues |
Queue management | Actions/Queue/ |
/realtime |
WebSocket management | Actions/Realtime/ |
/voide |
Voice AI code assistant | Actions/Buddy/ |
ORM-Generated CRUD Routes
Models with traits.useApi automatically get CRUD routes via ORM actions:
| Method | Path | Handler | Status |
|---|---|---|---|
| GET | /api/{model} |
Generated index | 200 |
| POST | /api/{model} |
Generated store | 201 |
| GET | /api/{model}/{id} |
Generated show | 200 |
| PUT/PATCH | /api/{model}/{id} |
Generated update | 200 |
| DELETE | /api/{model}/{id} |
Generated destroy | 204 |
Generated store and update handlers accept declared fillable attributes and
the foreign-key attributes derived from belongsTo. For example,
belongsTo: ['Product', 'Customer'] accepts productId / product_id and
customerId / customer_id. Other undeclared fields remain guarded.
CLI Commands
buddy dev-- starts the API server with hot-reloadbuddy generate:api-types-- regenerates API TypeScript types from OpenAPI spec
Gotchas
- The
fetcheris a singleton instance -- state (headers, body, query params) is reset after each request viaresetState(), but calling chain methods without executing a request will accumulate state fetcher.withDigestAuth()uses SHA-256 internally despite the method namegenerateMD5-- this is a misnomer in the source codefetcheralways parses responses as JSON (response.json()) -- it will throw if the response is not valid JSON- Only the
'web'route file key loads at root/with no prefix; all other keys (including'api') auto-prefix with/<key>. The'api'auto-prefix was added in stacksjs/stacks#1835 so user routes line up with the rpx proxy forward path - The
routeobject is a global singleton fromstacks-router.ts-- all route definitions across files share the same router instance - String-based handlers (
'Actions/MyAction') are resolved lazily at request time, not at registration time -- import errors surface only when the route is hit - Actions must export
defaultwith ahandle()method; Controllers must exportdefaultas a class with the specified method (defaults toindex) - The
requestproxy from@stacksjs/routerreturns safe defaults (null, undefined, empty) when accessed outside of a request context -- it warns in non-production JsonResource.filterAndResolve()has a max recursion depth of 20 to prevent stack overflow on circular referencesResourceCollection.toResponse()andJsonResource.toResponse()both wrap data in{ data: ... }by default -- callMyResource.withoutWrapping()to disable- The error handler tracks up to 50 recent queries in a circular buffer -- these are cleared after each successful response
- Middleware runs in registration order (not by priority) within a single route -- the
priorityfield onMiddlewareis for documentation/ordering within files - Middleware parameters (e.g.,
'abilities:read,write') are stored onrequest._middlewareParams-- middleware must parse the param string itself - The dev server's file watchers use Node's
fs.watch(notchokidar) -- on macOS this uses FSEvents, on Linux it uses inotify openapi.jsonstarts empty ({}) and is only populated whengenerateOpenApi()is explicitly called- The
api-types.tsfile uses operations keyed by action path (e.g.,'Actions/UserIndexOrmAction'), not by route name - CORS headers (
Access-Control-Allow-Origin: http://localhost:5173) are only added in development mode for error responses - Body parsing happens once per request (tracked via
_bodyParsedflag) -- JSON, URL-encoded, and multipart/form-data are all supported response.json()catches circular reference errors and returns{ error: 'Response data could not be serialized' }instead of throwing