Express Service Layers
Separates Express route handling, validation, and business logic into clear service boundaries.
Scenario
You maintain an Express API where HTTP handling, business logic, and persistence have separate responsibilities.
The architecture uses:
Route / Controller
↓
Service
↓
Repository
↓
Database
The separation exists so business logic does not become tightly coupled to Express or database implementation details.
You want coding agents to preserve these boundaries when adding endpoints or modifying existing behavior.
Repository Structure
api/
├── src/
│ ├── routes/
│ │ └── orders.ts
│ ├── controllers/
│ │ └── order-controller.ts
│ ├── services/
│ │ └── order-service.ts
│ ├── repositories/
│ │ └── order-repository.ts
│ ├── schemas/
│ │ └── order-schema.ts
│ └── server.ts
├── tests/
└── AGENTS.md
AGENTS.md
# Project Instructions
## API Architecture
Keep responsibilities separated:
- Routes define endpoint wiring and middleware.
- Controllers handle HTTP request and response concerns.
- Services contain application business logic.
- Repositories contain persistence logic.
- Schemas validate external input.
## Controllers
- Keep controllers thin.
- Do not put reusable business rules in controllers.
- Validate or consume validated request input before calling services.
- Preserve the existing API response and error conventions.
## Services
- Put business decisions and workflows in services.
- Services should not depend directly on Express request or response objects.
- Reuse existing services when behavior already exists.
## Repositories
- Access application persistence through repository modules.
- Do not execute database queries directly from controllers.
- Keep database-specific concerns out of HTTP handlers.
## Testing
- Test business behavior at the appropriate service or API level.
- Add or update API tests when endpoint behavior changes.
## Validation
Before completing source changes, run:
- `pnpm lint`
- `pnpm typecheck`
- relevant tests
What This Does
This gives each layer a clear responsibility.
For example, a controller might coordinate an order request:
export async function createOrderController(
req: Request,
res: Response,
) {
const order = await createOrder(req.body);
return res.status(201).json(order);
}
The business workflow belongs in the service:
export async function createOrder(input: CreateOrderInput) {
// business rules
// pricing decisions
// repository operations
}
And database access remains inside a repository.
This makes the dependency direction explicit.
What This Does NOT Do
The instructions do not require every operation to pass through unnecessary layers.
For example, they do not say:
- Every function must have a controller, service, and repository.
A route that returns static health information may not require a service or repository.
The architecture should reflect actual responsibilities rather than become ceremony.
The instructions also do not define exactly how every service function must be implemented.
Why These Instructions Matter
Without explicit boundaries, endpoint implementations often grow like this:
router.post("/orders", async (req, res) => {
// validate request
// query customer
// calculate pricing
// check inventory
// insert order
// send notification
// construct response
});
It works initially.
But business behavior is now tied to:
Express
HTTP
database access
Testing and reuse become harder.
The instruction:
- Put business decisions and workflows in services.
guides the agent toward the existing architecture rather than the shortest local implementation.
Key Decisions
Controllers handle HTTP
Things such as:
request
response
status code
headers
HTTP error mapping
belong naturally near the controller layer.
Services handle business decisions
Rules such as:
Can this order be created?
What discount applies?
Should inventory be reserved?
belong in application logic.
Repositories own persistence
A repository provides a boundary around database operations.
That makes persistence decisions less likely to leak into HTTP code.
Don't create layers mechanically
Architecture is useful when it separates meaningful responsibilities.
If a layer adds no responsibility, introducing it solely to satisfy a pattern can make the code harder to follow.
When to Use This Pattern
Use this pattern when:
- an Express API contains meaningful business logic;
- endpoints share workflows;
- database access needs clear ownership;
- business logic should be testable independently of HTTP;
- route handlers are becoming large.
For very small APIs, fewer layers may be appropriate. AGENTS.md should describe the architecture the repository actually uses.