Backend API Architecture Skill
name: backend-developer
by bayudsatriyo · published 2026-04-01
$ claw add gh:bayudsatriyo/bayudsatriyo-backend-developer---
name: backend-developer
description: Standardized backend REST API development following layered architecture patterns (Route → Controller → Service → Repository). Use when building new REST APIs, implementing features, fixing bugs, or refactoring backend code. Enforces strict separation of concerns, centralized error handling, input validation, DTO/mapper patterns, and Prisma ORM usage.
---
# Backend API Architecture Skill
This skill provides a standardized, production-ready architecture for Node.js/TypeScript REST APIs following the **4-layer pattern** proven across multiple production systems.
Core Architecture
Client Request
↓
[routes/] HTTP handlers + middleware
↓
[controller/] Request parsing + response formatting
↓
[service/] Business logic + orchestration
↓
[repository/] Database access (Prisma)Each layer has a single responsibility and clear boundaries.
Quick Start: The Standard Stack
**Runtime:** Node.js + TypeScript
**Framework:** Express
**ORM:** Prisma
**Validation:** Joi + Zod (request validation)
**Error Handling:** Custom `AppError` class + centralized middleware
**Response Format:** Standardized JSON with `{status, data, meta, error}`
File Organization
Each feature gets a **feature module** with 6 files:
src/
├── app/
│ └── [feature]/
│ ├── [feature].route.ts ← HTTP routes + middleware
│ ├── [feature].controller.ts ← Request → response
│ ├── [feature].service.ts ← Business logic
│ ├── [feature].repository.ts ← Database queries
│ ├── [feature].dto.ts ← TypeScript types
│ ├── [feature].mapper.ts ← Entity → DTO transformation
│ └── [feature].request.ts ← Joi/Zod validation schemas
├── config/
│ └── config.ts ← Environment loading
├── interface/
│ └── index.ts ← Global types, ERROR_CODE, ApiResponse
├── middleware/
│ ├── auth-middleware.ts
│ ├── error-handler.ts ← Centralized error handling
│ ├── validate-request.ts
│ ├── security.middleware.ts
│ └── index.ts
├── lib/
│ └── prisma.ts ← Prisma client singleton
├── utils/
│ ├── response-handler.ts ← ResponseHandler utilities
│ ├── handle-prisma-error.ts
│ └── clean-joi-error-message.ts
├── routes/
│ └── index.ts ← Central route aggregator
└── index.ts ← Express app setupThe 4 Layers Explained
1. Route Layer (`[feature].route.ts`)
**Responsibility:** HTTP method binding, middleware ordering, parameter extraction
**Does:** Apply auth middleware → validate input → call controller → error handling
**Does NOT:** Business logic, database access
export const [feature]Routes = express.Router();
[feature]Routes.post(
'/',
auth('ACCESS', [Roles.Admin]), // ← Auth middleware
validate(createSchema, 'body'), // ← Validation middleware
catchAsync([feature]Controller.create), // ← Error wrapping
);
[feature]Routes.get(
'/:id',
auth('ACCESS', [Roles.User, Roles.Admin]),
catchAsync([feature]Controller.findById),
);**Key utilities:**
2. Controller Layer (`[feature].controller.ts`)
**Responsibility:** Extract request data, call service, format response
**Does:** `req.body`, `req.query`, `req.params` → service call → `ResponseHandler.ok()`
**Does NOT:** Business logic, database queries
export const [feature]Controller = {
create: async (req: Request, res: Response, next: NextFunction) => {
const { body } = req;
const result = await [feature]Service.create(body);
// Service returns AppError or data
if (result instanceof AppError) {
next(result);
return;
}
ResponseHandler.created(res, result, 'Created successfully');
},
findAll: async (req: Request, res: Response, next: NextFunction) => {
const { query } = req;
const { data, meta } = await [feature]Service.findAll(query);
if (data instanceof AppError) {
next(data);
return;
}
ResponseHandler.ok(res, data, 'Fetched successfully', meta);
},
};**Pattern:**
1. Extract `req` data
2. Call service (which returns `AppError | data`)
3. Check for `AppError` → `next(error)`
4. Otherwise → `ResponseHandler.ok()` or `.created()`
3. Service Layer (`[feature].service.ts`)
**Responsibility:** Business logic, data orchestration, mapper usage
**Does:** Calls repository → transforms data (via mappers) → returns `AppError | data`
**Does NOT:** Direct database queries, HTTP handling
export const [feature]Service = {
create: async (input: CreateDto): Promise<AppError | [Feature]Dto> => {
// Validate business rules (not input format — that's the request layer)
const existing = await [feature]Repository.findByEmail(input.email);
if (existing) {
return new AppError('CONFLICT', 'Email already exists');
}
// Call repository
const entity = await [feature]Repository.create(input);
// Transform entity to DTO via mapper
return [feature]Mapper.toDtoArray([entity])[0];
},
findAll: async (query: QueryParams) => {
const { page = 1, perPage = 10 } = query;
const result = await [feature]Repository.findAll(page, perPage);
return {
data: [feature]Mapper.toDtoArray(result.data),
meta: {
currentPage: page,
totalPages: Math.ceil(result.count / perPage),
perPage,
totalEntries: result.count,
},
};
},
};**Pattern:**
4. Repository Layer (`[feature].repository.ts`)
**Responsibility:** Raw database access via Prisma
**Does:** `prisma.model.query()` — nothing else
**Does NOT:** Business logic, data transformation
export const [feature]Repository = {
create: async (input: CreateDto) => {
return prisma.[feature].create({
data: input,
});
},
findAll: async (page: number, perPage: number) => {
const skip = (page - 1) * perPage;
const [data, count] = await Promise.all([
prisma.[feature].findMany({
skip,
take: perPage,
where: { deletedAt: null }, // Soft delete filter
}),
prisma.[feature].count({
where: { deletedAt: null },
}),
]);
return { data, count };
},
};**Pattern:**
DTO & Mapper Pattern
**DTO** (Data Transfer Object) — TypeScript interface defining what data leaves the system:
// [feature].dto.ts
export interface [Feature]Dto {
id: string;
name: string;
email: string;
createdAt: Date;
}**Mapper** — Transform database entity to DTO:
// [feature].mapper.ts
export const [feature]Mapper = {
toDto(entity: [FeatureEntity]): [Feature]Dto {
return {
id: entity.id,
name: entity.name,
email: entity.email,
createdAt: entity.createdAt,
};
},
toDtoArray(entities: [FeatureEntity][]): [Feature]Dto[] {
return entities.map(e => this.toDto(e));
},
};Error Handling
**Central error class:**
export class AppError extends Error {
constructor(
public readonly code: ErrorCode, // 'BAD_REQUEST', 'UNAUTHORIZED', etc.
message?: string,
) {
super(message);
}
}**Error codes (from `interface/index.ts`):**
export const ERROR_CODE = {
BAD_REQUEST: { code: 'BAD_REQUEST', message: 'Bad Request', httpStatus: 400 },
UNAUTHORIZED: { code: 'UNAUTHORIZED', message: 'Unauthorized', httpStatus: 401 },
FORBIDDEN: { code: 'FORBIDDEN', message: 'Forbidden', httpStatus: 403 },
NOT_FOUND: { code: 'NOT_FOUND', message: 'Not Found', httpStatus: 404 },
CONFLICT: { code: 'CONFLICT', message: 'Resource already exists', httpStatus: 409 },
INTERNAL_SERVER_ERROR: { code: 'INTERNAL_SERVER_ERROR', httpStatus: 500 },
// ... extend as needed
};**Centralized error handler middleware:**
export const errorHandler: ErrorRequestHandler = (
err: AppError | Error,
req: Request,
res: Response,
) => {
if (err instanceof AppError) {
return res.status(err.httpStatus).json({
status: 'error',
error: {
code: err.code,
message: err.message,
},
});
}
// Fallback for unhandled errors
console.error(err.stack);
res.status(500).json({
status: 'error',
error: { code: 'INTERNAL_SERVER_ERROR', message: 'Internal Server Error' },
});
};Response Format
**Success response:**
{
"status": "success",
"message": "User created successfully",
"data": { "id": "123", "name": "John", "email": "john@example.com" },
"meta": null
}**Paginated response:**
{
"status": "success",
"data": [{ "id": "1" }, { "id": "2" }],
"meta": {
"currentPage": 1,
"totalPages": 5,
"perPage": 10,
"totalEntries": 42
}
}**Error response:**
{
"status": "error",
"error": {
"code": "NOT_FOUND",
"message": "User not found"
}
}Validation Pattern
**Request file defines Joi schema + TypeScript type:**
// [feature].request.ts
import Joi from 'joi';
export const createSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
phone: Joi.string().optional(),
});
export type CreateRequest = {
name: string;
email: string;
phone?: string;
};**Route uses it:**
[feature]Routes.post(
'/',
validate(createSchema, 'body'), // Validates against schema
catchAsync([feature]Controller.create),
);**Controller receives typed input:**
create: async (req: Request, res: Response) => {
const body = req.body as CreateRequest; // Already validated + typed
// ...
};Middleware Stack
Standard middleware order in `index.ts`:
app.use(express.json());
app.use(cors());
app.use(securityHeaders());
app.use(requestLogger());
app.use('/api', routes);
app.use(errorHandler); // Must be lastAuthentication Pattern
JWT middleware extracts and verifies token:
export const auth = (tokenType: 'ACCESS' | 'REFRESH', roles?: Role[]) => {
return async (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return next(new AppError('UNAUTHORIZED'));
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
if (roles && !roles.includes(payload.role)) {
return next(new AppError('FORBIDDEN'));
}
(req as any).user = payload;
next();
} catch {
next(new AppError('UNAUTHORIZED'));
}
};
};Key Utilities
See `references/utilities.md` for helper functions:
When Implementing Features
1. **Define request schema** — `[feature].request.ts`
2. **Define DTOs** — `[feature].dto.ts`
3. **Create repository** — `[feature].repository.ts` (Prisma queries only)
4. **Create service** — `[feature].service.ts` (business logic)
5. **Create controller** — `[feature].controller.ts` (request/response)
6. **Create mapper** — `[feature].mapper.ts` (entity → DTO)
7. **Create routes** — `[feature].route.ts` (HTTP endpoints)
8. **Register routes** — Add to `routes/index.ts`
Checklist Before Merge
References
Senior Engineer Standard
Before implementing anything, read `references/senior-engineer-mindset.md`. It defines the quality bar expected of every implementation:
An implementation that passes functionality but violates these principles is **not complete**.
More tools from the same signal band
Order food/drinks (点餐) on an Android device paired as an OpenClaw node. Uses in-app menu and cart; add goods, view cart, submit order (demo, no real payment).
Sign plugins, rotate agent credentials without losing identity, and publicly attest to plugin behavior with verifiable claims and authenticated transfers.
The philosophical layer for AI agents. Maps behavior to Spinoza's 48 affects, calculates persistence scores, and generates geometric self-reports. Give your...