# hydro AI context Use this file as the first context block when asking an AI coding tool to add or modify hydro resources in a Nuxt codebase. hydro creates resource-oriented REST APIs for Nuxt and Nitro. A resource definition generates CRUD routes, request validation, query parsing, relationship writes, custom operations, RFC 7807 errors, and OpenAPI docs. ## Install ```bash npm install @muffe/hydro zod ``` ## Nuxt configuration ```ts // nuxt.config.ts export default defineNuxtConfig({ modules: ['@muffe/hydro'], hydro: { prefix: '/api', resourcesDir: 'server/resources', openapi: { enabled: true, info: { title: 'My API', version: '1.0.0' }, }, }, }) ``` ## Resource file convention Put one resource per file in `server/resources/`. Export `defineResource()` as the default export. ```ts // server/resources/book.ts import { defineResource } from '#hydro' import { z } from 'zod' interface Book { id?: string title: string authorId?: string available: boolean } const books: Book[] = [] export default defineResource({ name: 'Book', path: 'books', schema: z.object({ id: z.string().optional(), title: z.string().min(1).max(200), authorId: z.string().optional(), available: z.boolean().default(true), }), filters: ['title', 'authorId', 'available'], pagination: { default: 10, max: 50 }, provider: { async list(ctx) { let items = [...books] if (ctx.query.filters.title) { items = items.filter(book => book.title.includes(String(ctx.query.filters.title))) } if (ctx.query.filters.authorId) { items = items.filter(book => book.authorId === String(ctx.query.filters.authorId)) } if (ctx.query.filters.available !== undefined) { items = items.filter(book => String(book.available) === String(ctx.query.filters.available)) } for (const sort of [...ctx.query.sort].reverse()) { items = items.sort((left, right) => { const leftValue = String(left[sort.field as keyof Book] ?? '') const rightValue = String(right[sort.field as keyof Book] ?? '') const result = leftValue.localeCompare(rightValue) return sort.direction === 'asc' ? result : -result }) } const { offset, itemsPerPage } = ctx.query.pagination const page = items.slice(offset, offset + itemsPerPage) return { items: selectFields(page, ctx.query.fields), total: items.length, } }, async get(ctx) { return books.find(book => book.id === ctx.params.id) ?? null }, }, processor: { async create(ctx) { const book = { available: true, ...ctx.input, id: crypto.randomUUID() } as Book books.push(book) return book }, async update(ctx) { const index = books.findIndex(book => book.id === ctx.params.id) const next = { ...books[index], ...ctx.input, id: ctx.params.id } as Book books[index] = next return next }, async delete(ctx) { const index = books.findIndex(book => book.id === ctx.params.id) if (index >= 0) books.splice(index, 1) }, }, }) function selectFields(items: T[], fields?: string[]): Array> { if (!fields?.length) return items return items.map((item) => { const selected: Partial = {} for (const field of fields) { if (field in item) selected[field as keyof T] = item[field as keyof T] } return selected }) } ``` ## Generated CRUD routes For `path: 'books'` and `prefix: '/api'`, hydro generates: ```http GET /api/books GET /api/books/:id POST /api/books PATCH /api/books/:id DELETE /api/books/:id ``` `GET /api/books` returns the list body as JSON and includes pagination metadata in headers when provided by the provider: ```http X-Total-Count: 42 X-Has-More: true ``` ## Provider and processor responsibilities Provider handles reads: ```ts provider: { async list(ctx) { return { items, total } }, async get(ctx) { return item ?? null }, } ``` Processor handles writes: ```ts processor: { async create(ctx) { return created }, async update(ctx) { return updated }, async delete(ctx) { // No return needed. Hydro responds with 204. }, } ``` ## Context object All handlers receive `ctx`. ```ts ctx.params.id // Route id from /books/:id ctx.query.filters // Parsed allow-listed filters ctx.query.sort // [{ field, direction }] ctx.query.pagination // { page, itemsPerPage, offset } ctx.query.fields // Sparse fieldset string[] or undefined ctx.input // Validated request body for writes and custom ops ctx.auth // Auth info when auth is enabled ctx.request // Request info with method, url, headers, raw ``` Example usage: ```ts async list(ctx) { const { filters, sort, pagination, fields } = ctx.query const rows = await db.books.findMany({ where: { title: filters.title ? String(filters.title) : undefined }, skip: pagination.offset, take: pagination.itemsPerPage, }) return { items: applyFields(rows, fields), total: await db.books.count() } } async create(ctx) { return db.books.create({ data: ctx.input }) } ``` ## Query parameters Declare filterable fields on the resource: ```ts export default defineResource({ // ... filters: ['title', 'authorId', 'available'], }) ``` Supported query parameters: ```http GET /api/books?title=Dune GET /api/books?authorId=a1&available=true GET /api/books?order[title]=asc GET /api/books?page=2&itemsPerPage=25 GET /api/books?fields=id,title,available GET /api/books?title=Dune&order[title]=asc&page=1&itemsPerPage=10&fields=id,title ``` Parsed shape: ```ts // GET /api/books?title=Dune&order[title]=asc&page=2&itemsPerPage=10&fields=id,title ctx.query = { filters: { title: 'Dune' }, sort: [{ field: 'title', direction: 'asc' }], pagination: { page: 2, itemsPerPage: 10, offset: 10 }, fields: ['id', 'title'], } ``` ## Relationships Relationships live in the resource definition. ### belongsTo reference writes Use `belongsTo` on the resource that stores the foreign key. ```ts // server/resources/book.ts export default defineResource({ name: 'Book', path: 'books', schema: z.object({ id: z.string().optional(), title: z.string().min(1), authorId: z.string().optional(), }), relationships: { author: { kind: 'belongsTo', target: 'Author', foreignKey: 'authorId', writable: { create: 'reference', update: 'reference' }, }, }, }) ``` Client request: ```http POST /api/books Content-Type: application/json { "title": "The Fifth Season", "author": "/authors/a5" } ``` Hydro verifies Author `a5` exists, removes `author`, and gives the processor `ctx.input` with `authorId: 'a5'`. ### hasMany nested writes Use `hasMany` on the parent resource. ```ts // server/resources/author.ts export default defineResource({ name: 'Author', path: 'authors', schema: z.object({ id: z.string().optional(), name: z.string().min(1), country: z.string().length(2), books: z.array(z.object({ title: z.string().min(1) })).optional(), }), relationships: { books: { kind: 'hasMany', target: 'Book', foreignKey: 'authorId', writable: { create: 'nested', update: false }, }, }, }) ``` Client request: ```http POST /api/authors Content-Type: application/json { "name": "Martha Wells", "country": "US", "books": [{ "title": "All Systems Red" }] } ``` Hydro creates the Author first, then validates and creates each nested Book with the new `authorId`. Writable modes: ```ts writable: { create: 'reference', update: 'reference' } // Accept string id or path writable: { create: 'nested', update: false } // Accept inline objects on create writable: { create: 'either', update: 'either' } // Accept references or inline objects writable: { create: false, update: false } // Field is not writable ``` ## Custom operations Use custom operations for behavior that is not plain CRUD. They validate input and output and appear in OpenAPI. ### Item operation ```ts customOperations: { checkout: { name: 'checkout', method: 'POST', scope: 'item', path: 'checkout', input: z.object({ userId: z.string().min(1) }), output: z.object({ id: z.string().optional(), available: z.boolean() }), async handler(ctx) { const book = books.find(book => book.id === ctx.params.id)! book.available = false return { id: book.id, available: book.available } }, }, } ``` Generated route: ```http POST /api/books/:id/checkout ``` ### Collection operation ```ts customOperations: { spotlight: { name: 'spotlight', method: 'POST', scope: 'collection', path: 'spotlight', input: z.object({ country: z.string().length(2).optional() }).optional(), output: z.array(z.object({ id: z.string().optional(), name: z.string(), bookCount: z.number(), })), async handler(ctx) { const country = ctx.input?.country?.toUpperCase() return authors .filter(author => author.featured && (!country || author.country === country)) .map(author => ({ id: author.id, name: author.name, bookCount: authorBookCount(author.id) })) }, }, } ``` Generated route: ```http POST /api/authors/spotlight ``` For item operations, `loadEntity: true` asks Hydro to call `provider.get` before the handler and provide `ctx.entity`. Missing entities return 404 automatically. ```ts checkout: { name: 'checkout', method: 'POST', scope: 'item', path: 'checkout', loadEntity: true, async handler(ctx) { return ctx.entity }, } ``` ## Errors Validation errors return RFC 7807 Problem Details as `application/problem+json`. ```http POST /api/authors Content-Type: application/json { "name": "", "country": "USA" } HTTP/1.1 422 Unprocessable Entity Content-Type: application/problem+json { "type": "https://hydro.dev/errors/validation", "title": "Validation Failed", "status": 422, "detail": "One or more fields failed validation", "violations": [ { "propertyPath": "/name", "message": "Required" }, { "propertyPath": "/country", "message": "Expected 2 characters" } ] } ``` Core error classes are exported from `@muffe/hydro/core`: ```ts import { ConflictError, ForbiddenError, HydroError, NotFoundError, UnauthorizedError, ValidationError, } from '@muffe/hydro/core' throw new ValidationError([{ propertyPath: '/email', message: 'Already taken' }]) throw new NotFoundError('Book not found') throw new ConflictError('Duplicate slug') throw new ForbiddenError('Not the owner') throw new UnauthorizedError('Login required') throw new HydroError(429, 'Too Many Requests', 'Try again later') ``` ## Auth Module config: ```ts export default defineNuxtConfig({ hydro: { auth: { enabled: true, provider: 'nuxt-auth-utils', defaultAccess: { development: 'public', production: 'deny' }, session: { userId: 'user.id', roles: 'user.roles' }, policies: { ownsResource({ auth, entity }) { return auth?.userId === entity?.ownerId }, }, }, }, }) ``` Resource access rules: ```ts export default defineResource({ // ... auth: { default: 'authenticated', operations: { list: 'public', create: { roles: ['editor'] }, update: { policy: 'ownsResource', needsEntity: true }, }, }, }) ``` Access values: ```ts 'public' 'authenticated' 'deny' { roles: ['editor'] } { permissions: ['books:update'] } { policy: 'ownsResource', needsEntity: true } ``` ## OpenAPI docs When enabled, Hydro generates: ```http GET /api/_openapi.json GET /api/_docs ``` Disable: ```ts hydro: { openapi: { enabled: false }, } ``` ## AI coding rules When modifying a foreign Nuxt codebase that uses hydro, follow these rules: 1. Look for `hydro` config in `nuxt.config.ts` to confirm prefix and resources directory. 2. Put resource files in the configured resources directory, usually `server/resources/`. 3. Import `defineResource` from `#hydro` inside Nuxt resource files. 4. Use Zod schemas unless the project already uses another Standard Schema library. 5. Keep reads in `provider` and writes in `processor`. 6. Use `ctx.query` instead of manually parsing query strings. 7. Use `ctx.input` as trusted validated input, but still handle business constraints. 8. Return `null` from `provider.get` for missing records. Hydro maps this to 404. 9. Return `{ items, total }` from `provider.list` when possible. 10. Use relationships for reference or nested writes instead of manually accepting both shapes in processors. 11. Use custom operations for actions that are not CRUD. 12. Use RFC 7807 error classes from `@muffe/hydro/core` for business errors. 13. Keep resource `name` PascalCase and `path` plural kebab-case. 14. Do not generate manual Nitro route files for CRUD endpoints that Hydro already generates. ## Copy-paste prompt ```md I am working in a Nuxt codebase that uses hydro. Use this context as the source of truth: https://hydro-playground.vercel.app/llms-full.txt Before coding: - Read nuxt.config.ts and confirm hydro.prefix and hydro.resourcesDir. - Inspect existing files in the resources directory and match their style. - Add or edit one resource per file. - Import defineResource from #hydro. - Use Zod schemas. - Keep reads in provider and writes in processor. - Use ctx.query for filters, sort, pagination and sparse fields. - Use ctx.input for validated request bodies. - Use relationships and customOperations when appropriate. - Verify with typecheck and relevant tests. ```