Service Layer Architecture
by @wpank
Controller-service-query layered API architecture with data enrichment and parallel fetching. Use when building REST APIs or GraphQL resolvers with clean separation of concerns. Triggers on API architecture, service layer, controller pattern, data enrichment, REST API.
clawhub install service-layer-architectureπ About This Skill
name: service-layer-architecture model: standard description: Controller-service-query layered API architecture with data enrichment and parallel fetching. Use when building REST APIs or GraphQL resolvers with clean separation of concerns. Triggers on API architecture, service layer, controller pattern, data enrichment, REST API.
Service Layer Architecture
Clean, performant API layers with proper separation of concerns and parallel data fetching.
When to Use
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 => {
// 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 => {
const entity = await queries.getEntityById(entityId);
if (!entity) return null;
return getEntityData(entity);
};
export const getEntities = async (): Promise => {
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 |