---
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/<module>/
├── types.ts          # Zod schemas + TypeScript interfaces
├── repository.ts     # Cosmos DB CRUD operations
├── routes.ts         # Fastify REST endpoints
└── <module>.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 create<Entity>Schema = 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 update<Entity>Schema = create<Entity>Schema.partial();

// ── Full document (stored in Cosmos) ──
export const <entity>Schema = create<Entity>Schema.extend({
  id: z.string().uuid(),
  userId: z.string(),
  productId: z.string(),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

// ── TypeScript types ──
export type Create<Entity> = z.infer<typeof create<Entity>Schema>;
export type Update<Entity> = z.infer<typeof update<Entity>Schema>;
export type <Entity> = z.infer<typeof <entity>Schema>;
```

### 2. Create `repository.ts`

```typescript
import { getCollection } from '../../lib/datastore.js';
import type { Create<Entity>, Update<Entity>, <Entity> } from './types.js';
import { NotFoundError } from '../../lib/errors.js';
import { randomUUID } from 'node:crypto';

const CONTAINER = '<product>_<entities>';

function getContainer() {
  return getCollection(CONTAINER);
}

export async function create<Entity>(
  userId: string,
  productId: string,
  data: Create<Entity>
): Promise<<Entity>> {
  const now = new Date().toISOString();
  const doc: <Entity> = {
    ...data,
    id: randomUUID(),
    userId,
    productId,
    createdAt: now,
    updatedAt: now,
  };
  const container = getContainer();
  await container.create(doc);
  return doc;
}

export async function list<Entities>(
  userId: string,
  productId: string
): Promise<<Entity>[]> {
  const container = getContainer();
  return container.query<Entity>({
    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 get<Entity>ById(
  id: string,
  userId: string,
  productId: string
): Promise<<Entity>> {
  const container = getContainer();
  const doc = await container.read<Entity>(id, userId);
  if (!doc || doc.productId !== productId) {
    throw new NotFoundError('<Entity> not found');
  }
  return doc;
}

export async function update<Entity>(
  id: string,
  userId: string,
  productId: string,
  data: Update<Entity>
): Promise<<Entity>> {
  const existing = await get<Entity>ById(id, userId, productId);
  const updated: <Entity> = {
    ...existing,
    ...data,
    updatedAt: new Date().toISOString(),
  };
  const container = getContainer();
  await container.replace(id, updated, userId);
  return updated;
}

export async function delete<Entity>(
  id: string,
  userId: string,
  productId: string
): Promise<void> {
  await get<Entity>ById(id, userId, productId); // Verify ownership
  const container = getContainer();
  await container.delete(id, userId);
}
```

### 3. Create `routes.ts`

```typescript
import type { FastifyInstance } from 'fastify';
import { create<Entity>Schema, update<Entity>Schema } from './types.js';
import * as repo from './repository.js';
import { getUserId, getRequestProductId } from '../../lib/request-context.js';

export default async function <entity>Routes(app: FastifyInstance) {
  // GET /api/<entities>
  app.get('/<entities>', async (req) => {
    const userId = getUserId(req);
    const productId = getRequestProductId(req);
    return repo.list<Entities>(userId, productId);
  });

  // GET /api/<entities>/:id
  app.get('/<entities>/:id', async (req) => {
    const { id } = req.params as { id: string };
    const userId = getUserId(req);
    const productId = getRequestProductId(req);
    return repo.get<Entity>ById(id, userId, productId);
  });

  // POST /api/<entities>
  app.post('/<entities>', async (req, reply) => {
    const userId = getUserId(req);
    const productId = getRequestProductId(req);
    const data = create<Entity>Schema.parse(req.body);
    const entity = await repo.create<Entity>(userId, productId, data);
    reply.code(201);
    return entity;
  });

  // PATCH /api/<entities>/:id
  app.patch('/<entities>/:id', async (req) => {
    const { id } = req.params as { id: string };
    const userId = getUserId(req);
    const productId = getRequestProductId(req);
    const data = update<Entity>Schema.parse(req.body);
    return repo.update<Entity>(id, userId, productId, data);
  });

  // DELETE /api/<entities>/:id
  app.delete('/<entities>/:id', async (req, reply) => {
    const { id } = req.params as { id: string };
    const userId = getUserId(req);
    const productId = getRequestProductId(req);
    await repo.delete<Entity>(id, userId, productId);
    reply.code(204);
  });
}
```

### 4. Register in `server.ts`

```typescript
import <entity>Routes from './modules/<entities>/routes.js';
// ...
await app.register(<entity>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(<entities>): add <entity> CRUD module

- types.ts: Zod schemas for create/update/<entity>
- repository.ts: Cosmos CRUD with user ownership
- routes.ts: GET/POST/PATCH/DELETE endpoints
- tests: N tests covering CRUD + auth + validation"
git push
```
