# Nodejs Architecture

> >

- **Type:** Skill
- **Install:** `agentstack add skill-j4flmao-agent-skills-architecture`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [j4flmao](https://agentstack.voostack.com/s/j4flmao)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [j4flmao](https://github.com/j4flmao)
- **Source:** https://github.com/j4flmao/agent-skills/tree/main/skills/backend/nodejs/architecture

## Install

```sh
agentstack add skill-j4flmao-agent-skills-architecture
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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)
```typescript
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:
```typescript
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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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

1. **Middleware ordering wrong**: Rate limiter before JSON parser causes body read issues. Security middleware (helmet, cors) must come first, before any body parsing.

2. **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](https://github.com/j4flmao)
- **Source:** [j4flmao/agent-skills](https://github.com/j4flmao/agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-j4flmao-agent-skills-architecture
- Seller: https://agentstack.voostack.com/s/j4flmao
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
