- roadmap-execution: phased roadmap execution with checkpoints - new-product-scaffold: scaffold new ByteLyst product repos - prd-to-implementation: convert PRDs to concrete plans - cross-repo-debug: systematic multi-repo debugging - backend-module-crud: Fastify CRUD modules (types/repo/routes/tests) - platform-integration: wire products into common platform - refactor-with-tests: test-first safe refactoring - test-gap-analysis: coverage gap identification and remediation - type-safety-sweep: TypeScript error triage and fix - dependency-health-check: cross-repo dependency audit - pre-release-validation: comprehensive release checklist - docker-production-prep: production Docker images - agents-md-sync: keep AI instruction files accurate - ecosystem-audit: full ecosystem health dashboard
247 lines
6.8 KiB
Markdown
247 lines
6.8 KiB
Markdown
---
|
|
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
|
|
```
|