scaffold-backend-slice
Skill3 starsChanged 11 days agoMITView on GitHub
Use for /scaffold-backend-slice <slice> <resources>. Creates a zod-validated route with typed errors, a domain service stub and colocated node:test files.
- Sends data out
In this repository3 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- add-shadcn-componentSkill
- scaffold-moduleSkill
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 1 is distinctive, and 11 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 21
Make sure that both names are kebab-case.
- Very specific: line 22
Make sure that src/server/routes/<resources>.route.ts and src/server/services/domain/<slice>.ts do not exist.
- Very specific: line 26
Write the service stub src/server/services/domain/<slice>.ts with this content.
- Very specific: line 46
Write src/server/services/domain/<slice>.test.ts with this content:
- Very specific: line 58
Write the route src/server/routes/<resources>.route.ts with this content:
- Very specific: line 89
Add import { <resourcesCamel>Router } from "./<resources>.route.js".
- Very specific: line 90
Add apiRouter.use(<resourcesCamel>Router).
- Very specific: line 92
Write the route test src/server/routes/<resources>-route.test.ts with this content:
- Very specific: line 155
Keep business logic in the service.
- Very specific: line 156
Do not call exec, tmux or git in the route.
- Very specific: line 157
Use .refine((s) => s.length <= MAX) for a length limit.
- Very specific: line 158
Put a node:test file next to each new route, service and domain file.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Scaffold a backend slice
The rules are in docs/standards/backend-design.md, in the sections "Layer definitions", "Validation", "Errors", "Naming" and "Tests".
Names
<slice>is the slice name in kebab-case. Example:note-draft.<Slice>is the same name in PascalCase. Example:NoteDraft.<sliceCamel>is the same name in camelCase. Example:noteDraft.<resources>is the plural resource name in kebab-case. Example:note-drafts. The route file uses this name.<resourcesCamel>is the plural name in camelCase. Example:noteDrafts.
Before you start
- Get the slice name and the resource name from the arguments of the command. If the resource name is missing, use the plural form of the slice name.
- Make sure that both names are kebab-case. If a name is not kebab-case, stop and tell the user.
- Make sure that
src/server/routes/<resources>.route.tsandsrc/server/services/domain/<slice>.tsdo not exist. If one exists, stop and tell the user.
Procedure
Write the service stub
src/server/services/domain/<slice>.tswith this content. A domain file does not import an adapter, the store, infra or orchestration.export interface <Slice>Input { title: string; } export interface <Slice>Result { title: string; length: number; } /** * Build the <slice> result from validated input. */ export function build<Slice>(input: <Slice>Input): <Slice>Result { return { title: input.title, length: input.title.length }; }Write
src/server/services/domain/<slice>.test.tswith this content:import test from "node:test"; import assert from "node:assert/strict"; import { build<Slice> } from "./<slice>.js"; test("build<Slice> returns the title and its length", () => { assert.deepEqual(build<Slice>({ title: "One" }), { title: "One", length: 3 }); });Write the route
src/server/routes/<resources>.route.tswith this content:import { Router } from "express"; import { z } from "zod"; import { parseOrThrow } from "./parse-input.js"; import { build<Slice> } from "../services/domain/<slice>.js"; const MAX_TITLE_LEN = 200; const invalidTitle = { error: "invalid-title" } as const; const <sliceCamel>Schema = z.object( { title: z .string(invalidTitle) .trim() .min(1, invalidTitle) .refine((title) => title.length <= MAX_TITLE_LEN, invalidTitle), }, invalidTitle, ); export const <resourcesCamel>Router = Router(); <resourcesCamel>Router.post("/<resources>", (req, res) => { const input = parseOrThrow(<sliceCamel>Schema, req.body); res.json(build<Slice>(input)); });- Mount the router in
src/server/routes/index.ts:- Add
import { <resourcesCamel>Router } from "./<resources>.route.js";after the other router imports. - Add
apiRouter.use(<resourcesCamel>Router);after the otherapiRouter.uselines.
- Add
Write the route test
src/server/routes/<resources>-route.test.tswith this content:import test, { after } from "node:test"; import assert from "node:assert/strict"; import type { Server } from "node:http"; import { isolateEnv } from "../test-support/fixtures.js"; const env = isolateEnv(); const express = (await import("express")).default; const { <resourcesCamel>Router } = await import("./<resources>.route.js"); const { httpErrorHandler } = await import("./error-handler.js"); const app = express(); app.use("/api", express.json(), <resourcesCamel>Router); app.use(httpErrorHandler); const server: Server = await new Promise((resolve) => { const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); const base = `http://127.0.0.1:${(server.address() as { port: number }).port}/api`; after(() => { server.close(); env.cleanup(); }); /** * Post a JSON body to the <resources> route and return the status and the body text. */ async function post(body: unknown): Promise<{ status: number; text: string }> { const res = await fetch(`${base}/<resources>`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); return { status: res.status, text: await res.text() }; } test("POST /<resources> returns the trimmed title", async () => { assert.deepEqual(await post({ title: " One " }), { status: 200, text: JSON.stringify({ title: "One", length: 3 }), }); }); test("POST /<resources> rejects an empty title with invalid-title", async () => { assert.deepEqual(await post({ title: " " }), { status: 400, text: JSON.stringify({ error: "invalid-title" }), }); }); test("POST /<resources> rejects an array body with invalid-title", async () => { assert.deepEqual(await post(["x"]), { status: 400, text: JSON.stringify({ error: "invalid-title" }), }); });
Rules for the next edits
- Parse each route input with a zod schema in the route file. Give each field schema its error code as the message.
- Throw a typed error from
src/server/services/domain/errors.ts, for exampleNotFoundError("<slice>-not-found"). Do not callres.statuswith a 4xx or 5xx code. - Keep business logic in the service. Do not read
reqorresin a service. - Do not call
exec, tmux or git in the route. Call the board store only throughboardRepositoryfromsrc/server/store/board-repository.ts. - Use
.refine((s) => s.length <= MAX)for a length limit. The zod.max()counts code points, not the JavaScriptlength. - Put a
node:testfile next to each new route, service and domain file.
Checks
Format the new files with env -u NODE_ENV npx oxfmt <each new file>. A long name can make a line too long for the template layout.
Then run these commands. Each command must pass.
env -u NODE_ENV npm run format:check
env -u NODE_ENV npm run lint
env -u NODE_ENV npm run typecheck
env -u NODE_ENV npm run depcruise
env -u NODE_ENV npm run test
env -u NODE_ENV npm run deadcodeDoes it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

