service-layer-architecture
npx machina-cli add skill wpank/ai/service-layer-architecture --openclawFiles (1)
SKILL.md
5.1 KB
Service Layer Architecture
Clean, performant API layers with proper separation of concerns and parallel data fetching.
Installation
OpenClaw / Moltbot / Clawbot
npx clawhub@latest install service-layer-architecture
When to Use
- Building REST APIs with complex data aggregation
- GraphQL resolvers needing data from multiple sources
- Any API where responses combine data from multiple queries
- Systems needing testable, maintainable code
Three-Layer Architecture
┌─────────────────────────────────────────────────────┐
│ Controllers │ HTTP handling, validation │
├─────────────────────────────────────────────────────┤
│ Services │ Business logic, data enrichment │
├─────────────────────────────────────────────────────┤
│ Queries │ Database access, raw data fetch │
└─────────────────────────────────────────────────────┘
Layer 1: Controllers (HTTP Only)
// controllers/Entity.ts
import { getEntity, getEntities } from "../services/Entity";
const router = new Router();
router.get("/entity/:entityId", async (ctx) => {
const { entityId } = ctx.params;
if (!entityId) {
ctx.status = 400;
ctx.body = { error: "Invalid entity ID" };
return;
}
const entity = await getEntity(entityId);
if (!entity) {
ctx.status = 404;
ctx.body = { error: "Entity not found" };
return;
}
ctx.status = 200;
ctx.body = entity;
});
Layer 2: Services (Business Logic)
// services/Entity.ts
import { queries } from "@common";
export const getEntityData = async (entity: RawEntity): Promise<EnrichedEntity> => {
// Parallel fetch all related data
const [metadata, score, activity, location] = await Promise.all([
queries.getMetadata(),
queries.getLatestScore(entity.id),
queries.getActivity(entity.id),
queries.getLocation(entity.slotId),
]);
// Transform and combine
return {
...entity,
bonded: entity.bonded / Math.pow(10, metadata.decimals),
total: score?.total ?? 0,
location: location?.city,
activity: {
activeCount: activity?.active?.length ?? 0,
inactiveCount: activity?.inactive?.length ?? 0,
},
};
};
export const getEntity = async (entityId: string): Promise<EnrichedEntity | null> => {
const entity = await queries.getEntityById(entityId);
if (!entity) return null;
return getEntityData(entity);
};
export const getEntities = async (): Promise<EnrichedEntity[]> => {
const all = await queries.allEntities();
const enriched = await Promise.all(all.map(getEntityData));
return enriched.sort((a, b) => b.total - a.total);
};
Layer 3: Queries (Database Access)
// queries/Entities.ts
import { EntityModel } from "../models";
export const allEntities = async () => {
return EntityModel.find({}).lean(); // Always use .lean()
};
export const getEntityById = async (id: string) => {
return EntityModel.findOne({ id }).lean();
};
export const validEntities = async () => {
return EntityModel.find({ valid: true }).lean();
};
Parallel Data Fetching
// BAD: Sequential (slow)
const metadata = await queries.getMetadata();
const score = await queries.getScore(id);
const location = await queries.getLocation(id);
// Time: sum of all queries
// GOOD: Parallel (fast)
const [metadata, score, location] = await Promise.all([
queries.getMetadata(),
queries.getScore(id),
queries.getLocation(id),
]);
// Time: max of all queries
Layer Responsibilities
| Task | Layer |
|---|---|
| Parse request params | Controller |
| Validate input | Controller |
| Set HTTP status | Controller |
| Combine multiple queries | Service |
| Transform data | Service |
| Sort/filter results | Service |
| Run database query | Query |
Related Skills
- Related: postgres-job-queue — Background job processing
- Related: realtime/websocket-hub-patterns — Real-time updates from services
NEVER Do
- NEVER put database queries in controllers — Violates separation
- NEVER put HTTP concerns in services — Services must be reusable
- NEVER fetch related data sequentially — Use Promise.all
- NEVER skip .lean() on read queries — 5-10x faster
- NEVER expose raw database errors — Transform to user-friendly messages
Source
git clone https://github.com/wpank/ai/blob/main/skills/backend/service-layer-architecture/SKILL.mdView on GitHub Overview
Service Layer Architecture splits HTTP handling, business logic, and data access into Controllers, Services, and Queries. It enables data enrichment and parallel fetching to produce richer responses while keeping code testable and maintainable.
How This Skill Works
Controllers handle HTTP input and validation, then delegate to Services. Services perform business logic and enrich responses by fetching related data in parallel (e.g., metadata, scores, activity, location) via Promise.all, then assemble the final enriched object. Queries access the database and return lean data, enabling fast, reusable primitives.
When to Use It
- Building REST APIs with complex data aggregation
- GraphQL resolvers needing data from multiple sources
- APIs whose responses combine data from multiple queries
- Systems requiring testable, maintainable code
- When you want performance through parallel data fetching
Quick Start
- Step 1: npx clawhub@latest install service-layer-architecture
- Step 2: Create Layer 1 Controllers, Layer 2 Services, and Layer 3 Queries as shown in the examples
- Step 3: Run the app and test endpoints; verify enriched responses and parallel fetch with Promise.all
Best Practices
- Keep controllers thin; delegate validation and processing to the service layer
- Use Promise.all to fetch related data in parallel in the service layer
- Enrich data in the service layer and transform it before returning
- Use lean() in queries to improve read performance
- Document layer responsibilities and maintain tests across Controllers, Services, and Queries
Example Use Cases
- REST API for a user profile that aggregates metadata, scores, activity, and location to present a rich profile.
- GraphQL resolver that composes data from multiple microservices into a single EnrichedEntity.
- Entity listing endpoint that returns sorted enriched entities based on total score.
- Input-validated REST endpoint that returns 400/404 errors when IDs are invalid or missing.
- Performance-optimized API using parallel data fetching to reduce latency.
Frequently Asked Questions
Add this skill to your agents