Skip to main content

stacks-api

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.

Source facts

Repository
stacksjs/wildloop
Last source activity
September 23, 2026 at 22:54
Detected SKILL.md language
English
Stars
2
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub