Development
New Feature Package
Step-by-step guide for adding a new SMS feature module. Use feature-farmer-registry and feature-farmer-registry-web as reference examples — they are the most complete implementations.
Decide on placement#
| If your code… | Place it in |
|---|---|
| Accesses the database or calls external APIs | be-packages/feature-<domain> |
| Renders React UI for a specific feature | fe-packages/feature-<domain>-web |
| Defines types/schemas shared between API and web | shared/<domain>-contracts |
| Is truly reusable cross-domain validation | shared/validation |
Implementation steps#
- 1
Create the backend package
Create the directory and required files:
bashmkdir -p be-packages/feature-<domain>/src/{db,schema,services,routes} touch be-packages/feature-<domain>/src/index.tsMinimum
package.json:json{ "name": "@repo/feature-<domain>", "version": "0.1.0", "type": "module", "private": true, "main": "./src/index.ts", "exports": { ".": "./src/index.ts" }, "dependencies": { "@repo/validation": "*" }, "devDependencies": { "@repo/typescript-config": "*", "@repo/eslint-config": "*", "typescript": "5.9.2", "vitest": "^3.2.4" } } - 2
Define Zod schemas first
In
src/schema/, define validation schemas before writing any service logic. Infer TypeScript types from schemas.typescript// src/schema/my-entity.schema.ts import { z } from 'zod'; export const CreateMyEntitySchema = z.object({ name: z.string().min(1), orgId: z.string().uuid(), }); export type CreateMyEntityInput = z.infer<typeof CreateMyEntitySchema>; - 3
Write Drizzle ORM schema
In
src/db/, define the Drizzle table schema and generate a migration:bashnpm run db:generate:<domain> - 4
Implement service functions
In
src/services/, write pure service functions. No HTTP — just data access via Drizzle + business logic. Services must acceptorgIdand scope all queries to it. - 5
Write route handlers
In
src/routes/, write Hono route handlers. Routes are thin: validate input → call service → return result.typescriptimport { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono'; import { CreateMyEntitySchema } from '../schema/my-entity.schema'; import { createMyEntity } from '../services/my-entity.service'; export const myEntityRoutes = new Hono() .post('/', zValidator('json', CreateMyEntitySchema), async (c) => { const { orgId } = c.get('auth'); const input = c.req.valid('json'); const entity = await createMyEntity({ ...input, orgId }); return c.json(entity, 201); }); - 6
Export from index.ts
typescript// src/index.ts — only export what consumers need export { myEntityRoutes } from './routes/my-entity.routes'; export { CreateMyEntitySchema, type CreateMyEntityInput } from './schema/my-entity.schema'; - 7
Wire into apps/api
Add the new package to
apps/api/package.jsondependencies and mount the routes inapps/api/src/index.ts:typescriptimport { myEntityRoutes } from '@repo/feature-<domain>'; app.route('/api/my-entities', myEntityRoutes); - 8
Create the frontend package
Create
fe-packages/feature-<domain>-web/with the same package.json pattern, using@repo/feature-farmer-registry-webas a reference. Add SWR hooks, API client helpers, and React components. - 9
Add @source for Tailwind
In
apps/web/app/globals.css, add a source entry so Tailwind v4 scans the new package's classes:css@source "../../../fe-packages/feature-<domain>-web/src/**/*.{ts,tsx}"; - 10
Verify and test
bash# From the repo root npm run typecheck npm run lint npm run test npm run build
Optional: shared contracts package#
If the API response types need to be consumed by both the frontend and the API (e.g. a complex response structure), extract them into shared/<domain>-contracts/. See shared/dashboard-contracts/ and shared/eudr-compliance-contracts/ as examples.
Checklist before merging#
- ☐No app-specific business logic in route pages.
- ☐No deep imports outside documented package exports.
- ☐npm run typecheck passes for all touched workspaces.
- ☐npm run lint passes with zero warnings.
- ☐At least one service unit test and one route smoke test are included.
- ☐New API routes have Zod validation on all inputs.
- ☐New fe-package is added as a @source in apps/web/app/globals.css.
be-packages/feature-farmer-registry and fe-packages/feature-farmer-registry-web are the most complete and well-tested packages in the codebase. Use them as the reference for structure, patterns, and naming.