- 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
6.8 KiB
6.8 KiB
| name | description | argument-hint | agent |
|---|---|---|---|
| backend-module-crud | Add a complete Fastify CRUD module with types, repository, routes, and tests following ByteLyst conventions. | Module name and parent repo, e.g. "habits in efforise", "workouts in peakpulse", "conversations in jarvisjr" | 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 middlewarezod— Schema validation (service's own copy, NOT from @bytelyst/config)
Implementation Steps
1. Create types.ts
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
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
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
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
cd backend
pnpm test
pnpm typecheck
pnpm build
7. Commit
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