Install
$ agentstack add skill-j4flmao-agent-skills-architecture ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Node.js Architecture
Purpose
Define and enforce Node.js backend architecture, framework selection, and middleware pipeline conventions.
Agent Protocol
Trigger
User request includes: node.js, nodejs, express, fastify, hono, node backend, node project structure, middleware, node.js routing.
Input Context
- Framework (Express, Fastify, Hono)
- Runtime (Node.js v18+, Bun, Deno)
- Language (JavaScript, TypeScript)
- Project type (REST API, GraphQL, BFF)
Output Artifact
A markdown document containing:
- Project structure
- Middleware pipeline ordering
- Routing conventions
- Error handling strategy
- Validation setup (Zod, Joi)
- Dependency injection pattern
- Testing setup
Response Format
Produce the artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output — why use many token when few do trick.
Completion Criteria
- Project structure follows separation of concerns
- Middleware pipeline ordered by responsibility
- Error handler catches all exceptions
- Validation integrated with schema library
Max Response Length
4096 tokens
Workflow
Step 1: Select Framework
| Framework | Performance | Ecosystem | TypeScript | When | |---|---|---|---|---| | Express | Moderate | Largest | Manual setup | Large ecosystem, legacy, most devs know it | | Fastify | High | Large | Native | Performance-critical, JSON schema validation | | Hono | Very High | Growing | Native | Edge, Bun, minimal footprint |
Step 2: Set Up Project Structure
src/
+-- modules/
| +-- orders/
| | +-- order.controller.ts
| | +-- order.service.ts
| | +-- order.repository.ts
| | +-- order.schema.ts # Zod validation
| | +-- order.routes.ts
| | +-- order.test.ts
| | +-- order.mapper.ts
| +-- products/
| | +-- ...
| +-- users/
| +-- ...
+-- common/
| +-- middleware/
| | +-- auth.ts
| | +-- error-handler.ts
| | +-- request-logger.ts
| | +-- rate-limiter.ts
| | +-- request-id.ts
| | +-- validate.ts
| +-- errors/
| | +-- app-error.ts
| | +-- not-found.ts
| +-- types/
| | +-- index.ts
| | +-- express.d.ts
+-- config/
| +-- database.ts
| +-- env.ts
| +-- redis.ts
| +-- logger.ts
+-- app.ts # Express/Fastify app setup
+-- server.ts # Entry point
Step 3: Order Middleware Pipeline (Express)
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
import { requestLogger } from './common/middleware/request-logger';
import { rateLimiter } from './common/middleware/rate-limiter';
import { notFoundHandler } from './common/middleware/not-found';
import { errorHandler } from './common/middleware/error-handler';
import { requestId } from './common/middleware/request-id';
import { routes } from './routes';
const app = express();
app.use(cors({ origin: process.env.CORS_ORIGIN, credentials: true }));
app.use(helmet());
app.use(compression());
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(requestId);
app.use(requestLogger);
app.use(rateLimiter);
app.use('/api/v1', routes);
app.use(notFoundHandler);
app.use(errorHandler);
For Fastify:
import Fastify from 'fastify';
import cors from '@fastify/cors';
import helmet from '@fastify/helmet';
import compress from '@fastify/compress';
import rateLimit from '@fastify/rate-limit';
const app = Fastify({ logger: true });
await app.register(cors, { origin: process.env.CORS_ORIGIN });
await app.register(helmet);
await app.register(compress);
await app.register(rateLimit, { max: 100, timeWindow: '1 minute' });
await app.register(routes, { prefix: '/api/v1' });
app.setNotFoundHandler((req, reply) => {
reply.status(404).send({ success: false, error: { code: 'NOT_FOUND', message: 'Route not found' } });
});
app.setErrorHandler((err, req, reply) => {
const status = err.statusCode || 500;
reply.status(status).send({ success: false, error: { code: err.code || 'INTERNAL', message: err.message } });
});
Step 4: Implement Error Handling
// common/errors/app-error.ts
export interface ErrorDetails {
code: string;
message: string;
details?: unknown;
stack?: string;
}
export class AppError extends Error {
public readonly statusCode: number;
public readonly code: string;
public readonly details: unknown;
public readonly isOperational: boolean;
constructor(statusCode: number, code: string, message: string, details?: unknown) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
this.isOperational = true;
Object.setPrototypeOf(this, new.target.prototype);
Error.captureStackTrace(this, this.constructor);
}
public toJSON(): ErrorDetails {
return {
code: this.code,
message: this.message,
details: this.details,
stack: process.env.NODE_ENV === 'development' ? this.stack : undefined,
};
}
}
export class NotFoundError extends AppError {
constructor(entity: string, id: string) {
super(404, 'NOT_FOUND', `${entity} with id ${id} not found`);
}
}
export class ValidationError extends AppError {
constructor(errors: unknown) {
super(400, 'VALIDATION', 'Validation failed', errors);
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(401, 'UNAUTHORIZED', message);
}
}
export class ForbiddenError extends AppError {
constructor(message = 'Forbidden') {
super(403, 'FORBIDDEN', message);
}
}
// common/middleware/error-handler.ts
import type { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/app-error';
import { logger } from '../../config/logger';
export function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction): void {
if (err instanceof AppError) {
logger.warn({ err, requestId: req.id, path: req.path }, 'Operational error');
res.status(err.statusCode).json({
success: false,
error: err.toJSON(),
});
return;
}
logger.error({ err, requestId: req.id, path: req.path }, 'Unhandled error');
res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : err.message,
},
});
}
Step 5: Validation with Zod
// modules/orders/order.schema.ts
import { z } from 'zod';
export const createOrderSchema = z.object({
customerId: z.string().uuid(),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
unitPrice: z.number().positive(),
})).min(1, 'At least one item required'),
shippingAddress: z.object({
street: z.string().min(1),
city: z.string().min(1),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
country: z.string().length(2),
}),
couponCode: z.string().optional(),
});
export type CreateOrderInput = z.infer;
// common/middleware/validate.ts
import type { Request, Response, NextFunction } from 'express';
import type { ZodSchema } from 'zod';
import { ValidationError } from '../errors/app-error';
export function validate(schema: ZodSchema, source: 'body' | 'query' | 'params' = 'body') {
return (req: Request, _res: Response, next: NextFunction): void => {
const result = schema.safeParse(req[source]);
if (result.success) {
req[source] = result.data;
next();
} else {
next(new ValidationError(result.error.issues));
}
};
}
// Usage in route
router.post('/', validate(createOrderSchema), orderController.create);
Step 6: Routing and Controller Pattern
// modules/orders/order.routes.ts
import { Router } from 'express';
import { OrderController } from './order.controller';
import { validate } from '../../common/middleware/validate';
import { createOrderSchema, updateOrderSchema } from './order.schema';
import { authenticate } from '../../common/middleware/auth';
const router = Router();
const controller = new OrderController();
router.use(authenticate);
router.get('/', controller.list);
router.get('/:id', controller.getById);
router.post('/', validate(createOrderSchema), controller.create);
router.put('/:id', validate(updateOrderSchema), controller.update);
router.delete('/:id', controller.delete);
export { router as orderRoutes };
// modules/orders/order.controller.ts
import type { Request, Response, NextFunction } from 'express';
import { OrderService } from './order.service';
export class OrderController {
constructor(private readonly orderService = new OrderService()) {}
list = async (req: Request, res: Response, next: NextFunction): Promise => {
try {
const result = await this.orderService.findAll(req.query);
res.json({ success: true, data: result });
} catch (err) {
next(err);
}
};
getById = async (req: Request, res: Response, next: NextFunction): Promise => {
try {
const result = await this.orderService.findById(req.params.id);
res.json({ success: true, data: result });
} catch (err) {
next(err);
}
};
create = async (req: Request, res: Response, next: NextFunction): Promise => {
try {
const result = await this.orderService.create(req.body);
res.status(201).json({ success: true, data: result });
} catch (err) {
next(err);
}
};
update = async (req: Request, res: Response, next: NextFunction): Promise => {
try {
const result = await this.orderService.update(req.params.id, req.body);
res.json({ success: true, data: result });
} catch (err) {
next(err);
}
};
delete = async (req: Request, res: Response, next: NextFunction): Promise => {
try {
await this.orderService.delete(req.params.id);
res.status(204).send();
} catch (err) {
next(err);
}
};
}
Step 7: Configuration Management
// config/env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default('15m'),
CORS_ORIGIN: z.string().default('http://localhost:3000'),
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
});
export type Env = z.infer;
let env: Env;
export function loadEnv(): Env {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('Invalid environment variables:', result.error.issues);
process.exit(1);
}
env = result.data;
return env;
}
export function getEnv(): Env {
if (!env) return loadEnv();
return env;
}
Step 8: Dependency Injection Container
// config/container.ts
import { OrderService } from '../modules/orders/order.service';
import { OrderRepository } from '../modules/orders/order.repository';
import { PaymentService } from '../modules/payments/payment.service';
import { NotificationService } from '../modules/notifications/notification.service';
import { Database } from './database';
export class Container {
private static instance: Container;
private readonly db: Database;
private readonly services = new Map();
private constructor() {
this.db = new Database();
}
static getInstance(): Container {
if (!Container.instance) {
Container.instance = new Container();
}
return Container.instance;
}
getOrderRepository(): OrderRepository {
if (!this.services.has('orderRepository')) {
this.services.set('orderRepository', new OrderRepository(this.db));
}
return this.services.get('orderRepository') as OrderRepository;
}
getOrderService(): OrderService {
if (!this.services.has('orderService')) {
this.services.set('orderService', new OrderService(
this.getOrderRepository(),
this.getPaymentService(),
this.getNotificationService(),
));
}
return this.services.get('orderService') as OrderService;
}
getPaymentService(): PaymentService {
if (!this.services.has('paymentService')) {
this.services.set('paymentService', new PaymentService());
}
return this.services.get('paymentService') as PaymentService;
}
getNotificationService(): NotificationService {
if (!this.services.has('notificationService')) {
this.services.set('notificationService', new NotificationService());
}
return this.services.get('notificationService') as NotificationService;
}
}
Step 9: Testing Setup
// modules/orders/order.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import { createApp } from '../app';
import { OrderService } from './order.service';
vi.mock('./order.service');
describe('Orders API', () => {
let app: Express.Application;
beforeEach(() => {
vi.clearAllMocks();
app = createApp('test');
});
describe('POST /api/v1/orders', () => {
it('creates order successfully', async () => {
const mockOrder = { id: '123', customerId: 'cust-1', items: [] };
vi.mocked(OrderService.prototype.create).mockResolvedValue(mockOrder);
const res = await request(app)
.post('/api/v1/orders')
.send({
customerId: '550e8400-e29b-41d4-a716-446655440000',
items: [{ productId: '550e8400-e29b-41d4-a716-446655440001', quantity: 2, unitPrice: 19.99 }],
shippingAddress: { street: '123 Main', city: 'NYC', zipCode: '10001', country: 'US' },
})
.expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data.id).toBe('123');
});
it('returns 400 for invalid input', async () => {
const res = await request(app)
.post('/api/v1/orders')
.send({ customerId: 'invalid' })
.expect(400);
expect(res.body.error.code).toBe('VALIDATION');
});
it('returns 401 without auth token', async () => {
const res = await request(app)
.post('/api/v1/orders')
.send({})
.expect(401);
});
});
describe('GET /api/v1/orders/:id', () => {
it('returns 404 for non-existent order', async () => {
vi.mocked(OrderService.prototype.findById).mockRejectedValue(
new NotFoundError('Order', '999')
);
await request(app)
.get('/api/v1/orders/999')
.expect(404);
});
});
});
Architecture Decision Trees
Framework Selection
Need high performance?
+-- Yes -> Need edge runtime?
| +-- Yes -> Hono (Bun, Deno, Cloudflare Workers)
| +-- No -> Fastify (JSON schema validation, high throughput)
+-- No -> Need largest ecosystem?
+-- Yes -> Express (most middleware, community packages)
+-- No -> Hono (modern, lightweight, good DX)
Request Validation Strategy
TypeScript project?
+-- Yes -> Zod (type inference, composable schemas)
+-- No -> Joi (mature, expressive API)
Project Structure Decision
Monorepo with shared types?
+-- Yes -> Use Nx or Turborepo, shared packages for types/schemas
+-- No -> Flat modules/ structure with domain grouping
Error Handling Approach
Single service deployment?
+-- Yes -> Centralized AppError hierarchy with status codes
+-- No -> Add correlation IDs, distributed tracing headers
Common Pitfalls
- Middleware ordering wrong: Rate limiter before JSON parser causes body read issues. Security middleware (helmet, cors) must come first, before any body parsing.
- Swallowing async errors: Express does not catch async promise rejections automatically. Always use
express-async-errors
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: j4flmao
- Source: j4flmao/agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.