--- name: backend-module-crud description: 'Add a complete Fastify CRUD module with types, repository, routes, and tests following ByteLyst conventions.' argument-hint: 'Module name and parent repo, e.g. "habits in efforise", "workouts in peakpulse", "conversations in jarvisjr"' agent: agent --- # Backend Module CRUD Prompt Create a complete Fastify backend module with Zod-validated types, Cosmos repository, REST routes, and comprehensive tests. ## Context — ByteLyst Backend Pattern Every product backend follows this module structure: ``` backend/src/modules// ├── types.ts # Zod schemas + TypeScript interfaces ├── repository.ts # Cosmos DB CRUD operations ├── routes.ts # Fastify REST endpoints └── .test.ts # Vitest tests ``` **Key packages used:** - `@bytelyst/datastore` — Cosmos DB abstraction (`getCollection()`) - `@bytelyst/errors` — Typed HTTP errors (`BadRequestError`, `NotFoundError`, etc.) - `@bytelyst/fastify-auth` — JWT auth middleware - `zod` — Schema validation (service's own copy, NOT from @bytelyst/config) ## Implementation Steps ### 1. Create `types.ts` ```typescript import { z } from 'zod'; // ── Create schema (input from API) ── export const createSchema = z.object({ // Required fields name: z.string().min(1).max(200), // Optional fields description: z.string().max(1000).optional(), // Enum fields status: z.enum(['active', 'paused', 'completed']).default('active'), }); // ── Update schema (partial, for PATCH) ── export const updateSchema = createSchema.partial(); // ── Full document (stored in Cosmos) ── export const Schema = createSchema.extend({ id: z.string().uuid(), userId: z.string(), productId: z.string(), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), }); // ── TypeScript types ── export type Create = z.inferSchema>; export type Update = z.inferSchema>; export type = z.inferSchema>; ``` ### 2. Create `repository.ts` ```typescript import { getCollection } from '../../lib/datastore.js'; import type { Create, Update, } from './types.js'; import { NotFoundError } from '../../lib/errors.js'; import { randomUUID } from 'node:crypto'; const CONTAINER = '_'; function getContainer() { return getCollection(CONTAINER); } export async function create( userId: string, productId: string, data: Create ): Promise<> { const now = new Date().toISOString(); const doc: = { ...data, id: randomUUID(), userId, productId, createdAt: now, updatedAt: now, }; const container = getContainer(); await container.create(doc); return doc; } export async function list( userId: string, productId: string ): Promise<[]> { const container = getContainer(); return container.query({ query: 'SELECT * FROM c WHERE c.userId = @userId AND c.productId = @productId ORDER BY c.createdAt DESC', parameters: [ { name: '@userId', value: userId }, { name: '@productId', value: productId }, ], }); } export async function getById( id: string, userId: string, productId: string ): Promise<> { const container = getContainer(); const doc = await container.read(id, userId); if (!doc || doc.productId !== productId) { throw new NotFoundError(' not found'); } return doc; } export async function update( id: string, userId: string, productId: string, data: Update ): Promise<> { const existing = await getById(id, userId, productId); const updated: = { ...existing, ...data, updatedAt: new Date().toISOString(), }; const container = getContainer(); await container.replace(id, updated, userId); return updated; } export async function delete( id: string, userId: string, productId: string ): Promise { await getById(id, userId, productId); // Verify ownership const container = getContainer(); await container.delete(id, userId); } ``` ### 3. Create `routes.ts` ```typescript import type { FastifyInstance } from 'fastify'; import { createSchema, updateSchema } from './types.js'; import * as repo from './repository.js'; import { getUserId, getRequestProductId } from '../../lib/request-context.js'; export default async function Routes(app: FastifyInstance) { // GET /api/ app.get('/', async (req) => { const userId = getUserId(req); const productId = getRequestProductId(req); return repo.list(userId, productId); }); // GET /api//:id app.get('//:id', async (req) => { const { id } = req.params as { id: string }; const userId = getUserId(req); const productId = getRequestProductId(req); return repo.getById(id, userId, productId); }); // POST /api/ app.post('/', async (req, reply) => { const userId = getUserId(req); const productId = getRequestProductId(req); const data = createSchema.parse(req.body); const entity = await repo.create(userId, productId, data); reply.code(201); return entity; }); // PATCH /api//:id app.patch('//:id', async (req) => { const { id } = req.params as { id: string }; const userId = getUserId(req); const productId = getRequestProductId(req); const data = updateSchema.parse(req.body); return repo.update(id, userId, productId, data); }); // DELETE /api//:id app.delete('//:id', async (req, reply) => { const { id } = req.params as { id: string }; const userId = getUserId(req); const productId = getRequestProductId(req); await repo.delete(id, userId, productId); reply.code(204); }); } ``` ### 4. Register in `server.ts` ```typescript import Routes from './modules//routes.js'; // ... await app.register(Routes, { prefix: '/api' }); ``` ### 5. Create tests Write tests covering: - ✅ Create with valid data → 201 - ✅ Create with invalid data → 400 - ✅ List returns user's items only - ✅ Get by ID returns correct item - ✅ Get by ID with wrong user → 404 - ✅ Update modifies fields + updatedAt - ✅ Delete removes item → 204 - ✅ Delete with wrong user → 404 ### 6. Validate ```bash cd backend pnpm test pnpm typecheck pnpm build ``` ### 7. Commit ```bash git add . git commit -m "feat(): add CRUD module - types.ts: Zod schemas for create/update/ - repository.ts: Cosmos CRUD with user ownership - routes.ts: GET/POST/PATCH/DELETE endpoints - tests: N tests covering CRUD + auth + validation" git push ```