Contributing and Developer Recipes
Step-by-step guides for the most common development tasks in this monorepo. All recipes follow the conventions described in Architecture and Backend.
General workflow
- Pull latest changes from
main. - Create a branch scoped to the package you are changing (
backend/,frontend/,docs/). - Implement changes following the conventions below.
- Run
tsc --noEmitin the affected package before committing. - Update docs in
/docs/content/en/for any behavior changes.
Recipe: Add a backend endpoint
Reference implementation: backend-js/src/app/api/users/create/.
1 — Create the folder structure
backend-js/src/app/api/<domain>/<action>/
dtos/
<action>.dto.ts
repository/
<action>.repo.ts
service/
<action>.service.ts
route.ts
types.tsFolder name convention: dtos/ (plural). No exceptions.
2 — Define domain types (types.ts)
export interface CreateFooDto {
name: string;
// ... validated fields
}
export interface FooRow {
id: number;
name: string;
created_at: string;
}3 — Write the DTO validator (dtos/<action>.dto.ts)
import { isString } from "@/app/lib/dto";
import { ValidationError } from "@/app/lib/errors";
import { CreateFooDto } from "../types";
export function validateCreateFooDto(body: unknown): CreateFooDto {
if (!body || typeof body !== "object") {
throw new ValidationError("Invalid request body");
}
const { name } = body as Record<string, unknown>;
if (!isString(name)) throw new ValidationError("name is required");
return { name: name as string };
}Rules:
- Function name:
validateXxxDto(body: unknown): XxxDto. - No SQL calls inside DTOs.
- Use guards from
src/app/lib/dto.ts:isString,isEmail,isNumber,isBoolean,isUserRole,isPackageValidStatus. - Throw
ValidationErrorfor invalid input.
4 — Write the repository (repository/<action>.repo.ts)
import { connect } from "@/app/config/dbConfig";
import { ResultSetHeader } from "mysql2";
import { FooRow } from "../types";
export async function insertFoo(name: string): Promise<number> {
const db = await connect();
const [result] = await db.execute<ResultSetHeader>(
"INSERT INTO foos (name) VALUES (?)",
[name]
);
return result.insertId;
}
export async function findFooById(id: number): Promise<FooRow | null> {
const db = await connect();
const [rows] = await db.query<(FooRow & import("mysql2").RowDataPacket)[]>(
"SELECT id, name, created_at FROM foos WHERE id = ?",
[id]
);
return rows[0] ?? null;
}Rules:
- Only SQL here. No business logic.
- Always use prepared statements (
?placeholders). - Never call the repository from
route.tsdirectly — go through the service.
5 — Write the service (service/<action>.service.ts)
import { NotFoundError } from "@/app/lib/errors";
import { insertFoo, findFooById } from "../repository/<action>.repo";
import { CreateFooDto, FooRow } from "../types";
export async function createFooService(dto: CreateFooDto): Promise<FooRow> {
const id = await insertFoo(dto.name);
const foo = await findFooById(id);
if (!foo) throw new NotFoundError("Foo not created");
return foo;
}Rules:
- Business logic only. No SQL.
- Throw errors from
src/app/lib/errors.ts.
6 — Write the route handler (route.ts)
import { requireAuth } from "@/app/lib/jwt";
import { handleError, res } from "@/app/lib/response";
import { validateCreateFooDto } from "./dtos/<action>.dto";
import { createFooService } from "./service/<action>.service";
import { USER_ROLES } from "@/app/types";
export async function POST(request: Request) {
try {
const auth = requireAuth(request, [USER_ROLES.admin]);
const body = await request.json();
const dto = validateCreateFooDto(body);
const result = await createFooService(dto);
return res.created(result);
} catch (error) {
return handleError("[POST /api/<domain>/<action>]", error);
}
}Rules:
requireAuth→validateDto→service→res.xxx.- No SQL, no business logic.
- Always pass the endpoint label to
handleError.
7 — Update docs
Add the new endpoint to docs/content/en/api/index.mdx under the appropriate group. Include request body and response shape. Mark anything unverified with > [!NOTE] Needs verification.
Recipe: Add a frontend API call
Reference: frontend-app/app/lib/api/auth-api.ts.
1 — Add request/response types
Create or update frontend-app/app/utils/types/api/<domain>.types.ts:
export interface CreateFooRequest { name: string }
export interface CreateFooResponse { id: number; name: string; created_at: string }2 — Add the API function
In frontend-app/app/lib/api/<domain>-api.ts:
import { apiClient } from "./helpers/client";
import type { CreateFooRequest, CreateFooResponse } from "../../utils/types/api/foo.types";
export async function createFoo(payload: CreateFooRequest): Promise<CreateFooResponse> {
const response = await apiClient.post<CreateFooResponse>("/foos/create", payload);
return response.data;
}3 — Add a React Query hook
In frontend-app/app/hooks/<domain>/use<Action>.ts:
import { useMutation } from "@tanstack/react-query";
import { createFoo } from "../../lib/api/foo-api";
export function useCreateFoo() {
return useMutation({ mutationFn: createFoo });
}For queries (GET), add a query key in app/query/keys/ and options in app/query/options/ following the existing pattern.
Recipe: Add a frontend page
1 — Create the page file
In the appropriate App Router group under frontend-app/app/:
- Authenticated pages go in
(main)/(pages)/<page-name>/page.tsx. - Auth pages go in
(auth)/.
2 — Wire data
Use existing hooks from app/hooks/. Add a new hook if needed (see above).
3 — Add loading state
Add a skeleton loader component at (main)/(pages)/<page-name>/components/loaders/<Page>.loader.tsx following the boneyard-js Skeleton pattern used in existing pages.
4 — Add to sidebar navigation
If the page should appear in the aside menu, add it to the MenuOptions component in (main)/components/AsideMenu/components/MenuOptions.tsx.
Recipe: Add a new docs section (EN + ES + EUS)
1 — Create content files
docs/content/en/<section>/index.mdx
docs/content/en/<section>/_meta.ts
docs/content/es/<section>/index.mdx
docs/content/es/<section>/_meta.ts
docs/content/eus/<section>/index.mdx
docs/content/eus/<section>/_meta.ts_meta.ts minimal shape:
export default { index: { title: "Section Title" } };2 — Register in root _meta.ts
Add <section>: { title: "..." } to docs/content/en/_meta.ts, docs/content/es/_meta.ts, and docs/content/eus/_meta.ts.
3 — Add nav item
In docs/app/[lang]/layout.tsx, add to navItems:
{ href: "/<section>", label: nav.<sectionKey> },4 — Add i18n strings
In docs/app/i18n/en.ts, add <sectionKey>: "Section Title" to navigation in both the DocsDictionary interface and the en object. Repeat for es.ts and eus.ts.
5 — Write content
Write English first. For ES and EUS, either write full translations or add a summary + note linking to the EN version:
> [!NOTE]
> The English version is the authoritative source. See [English version](/en/<section>) for full details.Recipe: Add a new supported locale
- Create
docs/app/i18n/<locale>.tsmatching the fullDocsDictionaryinterface. - Register the locale in
docs/app/i18n/index.tsin thegetDictionaryfunction. - Add
<locale>toSUPPORTED_LOCALESindocs/middleware.ts. - Add
<locale>togenerateStaticParamsindocs/app/[lang]/[[...mdxPath]]/page.tsxif used. - Create
docs/content/<locale>/with all section content files.
Keeping docs truthful
- Never document behavior you haven’t verified from the source code.
- Use
> [!NOTE] Needs verificationfor anything uncertain. - After adding an endpoint: verify its request/response against the actual
route.tsanddtos/files, then updatedocs/content/en/api/index.mdx. - After changing environment vars: update
docs/content/en/environment/index.mdx. - After changing the DB schema: update
docs/content/en/database/index.mdx.