---
name: backend-architecture
description: |-
  Enforces professional Node.js/Express backend architecture — API design,
  database modeling, security, auth, and production-readiness standards.
  TRIGGER when: user builds or modifies any backend route, API endpoint,
  database schema, auth system, middleware, or server configuration. Also
  trigger when user asks about JWT, CORS, MongoDB, SQL, environment variables,
  deployment security, or backend file structure.
  TRIGGER also when: user's backend is crashing, leaking errors, or has
  security gaps they want to fix.
  DO NOT TRIGGER when: user is working on frontend components, CSS, React
  state, or any client-side UI task with no backend involvement.
title: backend-architecture
canonical_url: https://skilld.dev/gh/mdzubair933/web-design-mastery-skills/backend-architecture
last_updated: 2026-09-26T14:40:41.000Z
---

> **Skill from skilld.dev.** Follow the instructions below for this session. You do not need to install anything.
>
> If the user asked to install this Skill, run `npx skilld install mdzubair933/web-design-mastery-skills/backend-architecture`. Install writes the Skill files into the project, so every session loads them.

# Backend Architecture

Enforces production-grade Node.js/Express backend patterns — clean MVC
structure, secure auth, professional API design, and hardened deployment.
Primary constraint: no backend feature is built without a defined security
boundary, centralized error handling, and environment-variable-only secrets.

## Core Rules

Rule: All secrets live in `.env` only — never in source code. Why: automated
bots scan public repositories 24/7; hardcoded credentials are stolen within
minutes of exposure, causing data breaches and massive cloud billing spikes.

Rule: Never connect frontend directly to the database. Why: this bypasses all
business logic, leaks connection strings to the browser, and removes the
security layer entirely. All queries go through the Express backend only.

Rule: Wrap every controller in try-catch and use centralized error middleware.
Why: unhandled exceptions crash the Node process silently — the server dies
with no error returned to the client, and the cause is invisible.

Rule: Never store JWT access tokens in localStorage on the client. Why: XSS
scripts read localStorage freely. Access tokens go in React state; refresh
tokens go in HttpOnly cookies set by the backend — invisible to JS.

Rule: Whitelist only your production server IP in the database firewall.
Why: open access (`0.0.0.0/0`) means any internet scanner can attempt
connections. A single leaked credential becomes a full breach.

## MVC Folder Structure (non-negotiable)

```
backend/
├── .env                    ← all secrets — never commit this
├── .gitignore              ← must include .env, node_modules/, dist/
└── src/
    ├── models/             ← Mongoose schemas or SQL table definitions
    ├── controllers/        ← business logic per resource (invoices, users, auth)
    ├── routes/             ← Express route declarations + HTTP method mapping
    ├── middleware/         ← auth checks, CORS, rate limiting, payload parsing
    └── services/           ← external utilities: PDF gen, email, Cron jobs
```

Each layer has one job:
- `models/` defines data shape — no logic.
- `controllers/` runs logic — no routing.
- `routes/` maps paths to controllers — no logic.
- `middleware/` intercepts requests — no business logic.
- `services/` handles external systems — no routing.

Never mix responsibilities across layers. Why: mixing makes bugs untraceable
and makes the codebase unmaintainable as it grows.

## API Design

**REST Naming Rules:**
- Base: `https://api.yourdomain.com`
- Versioning: always prefix with `/api/v1/` — prevents breaking integrations
  when routes evolve.
- Resources: plural nouns only (`/users`, `/invoices`, `/orders`)
- Identifiers: parameterized (`/:id`) — never expose raw database keys in URLs

**Standard Endpoint Map:**
```
GET    /api/v1/invoices         → list all invoices for authenticated user
GET    /api/v1/invoices/:id     → get one invoice
POST   /api/v1/invoices         → create new invoice
PUT    /api/v1/invoices/:id     → replace entire invoice
PATCH  /api/v1/invoices/:id     → update specific fields only
DELETE /api/v1/invoices/:id     → permanently delete invoice
```

**HTTP Methods — when to use which:**
| Method | Use | Behavior |
|--------|-----|----------|
| GET | Fetch data | Safe, idempotent, no body |
| POST | Create new record | Body contains payload |
| PUT | Replace entire resource | Body replaces all fields |
| PATCH | Update specific fields | Body contains only changed fields |
| DELETE | Remove record | Irreversible unless soft-delete implemented |

**Status Codes — use exact codes, not just 200:**
| Code | Meaning | When to use |
|------|---------|-------------|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST — new record written |
| 400 | Bad Request | Invalid payload, missing required fields |
| 401 | Unauthorized | No valid token or token expired |
| 403 | Forbidden | Authenticated but lacks permission |
| 404 | Not Found | Resource or route does not exist |
| 409 | Conflict | Duplicate (email already registered) |
| 500 | Server Error | Unhandled exception — should never reach client |

**Standard Response Wrapper:**
```json
// Success
{ "success": true, "data": { ... } }

// Error
{ "success": false, "errorCode": "INVOICE_NOT_FOUND", "message": "Invoice with this ID does not exist." }
```
Why: consistent wrappers allow the frontend to handle all responses with
one pattern — checking `success` flag before accessing `data`.

**Webhooks:** Use for payment events (Razorpay, Stripe). The external system
POSTs to your webhook URL on events — do not poll. Polling is slow,
resource-heavy, and misses events during downtime.

## Database Design

**SQL vs NoSQL — decide using this rule:**
| Project type | Database |
|-------------|----------|
| Complex relationships, transactions, e-commerce | PostgreSQL (SQL) |
| Flexible documents, user profiles, content-heavy | MongoDB (NoSQL) |
| Default for AI-assisted fast iteration | MongoDB + Mongoose |

**MongoDB Schema Rules:**
- Always define Mongoose schemas even though MongoDB is schema-less. Why:
  type-safety at the server layer prevents corrupt documents from being stored.
- Required fields, type validation, and unique constraints must be declared
  in the schema — never rely on the application layer to enforce these.
- Always index documents with `userId` to isolate user data. Why: queries
  without userId isolation will leak one user's data to another.

```javascript
// Correct schema with type safety and isolation
const InvoiceSchema = new Schema({
  userId:    { type: Schema.Types.ObjectId, ref: "User", required: true, index: true },
  amount:    { type: Number, required: true },
  status:    { type: String, enum: ["draft", "sent", "paid"], default: "draft" },
  createdAt: { type: Date, default: Date.now },
});
```

## Authentication & Security

**Auth Architecture:**
- Register → hash password with bcrypt → store hash only, never plaintext.
- Login → verify bcrypt hash → issue access token (short-lived, in JSON body)
  + refresh token (long-lived, in HttpOnly cookie).
- Protected routes → verify access token in `Authorization: Bearer` header.
- Token refresh → client sends HttpOnly cookie to `/auth/refresh` → new
  access token issued without re-login.

**JWT Token Rules:**
| Token | Storage | Expiry | Purpose |
|-------|---------|--------|---------|
| Access Token | React state / Context | 15 min | Authorize API requests |
| Refresh Token | HttpOnly cookie (backend sets) | 7–30 days | Mint new access tokens |

**CORS Configuration:**
```javascript
app.use(cors({
  origin: process.env.CLIENT_URL,  // never "*" in production
  credentials: true,               // required for HttpOnly cookies
}));
```
Why: wildcard CORS allows any domain to call your API — this defeats the
purpose of having a backend security boundary.

**Environment Variables — mandatory structure:**
```
PORT=50001
MONGO_URI=mongodb+srv://...
JWT_ACCESS_SECRET=<random 64-char string>
JWT_REFRESH_SECRET=<random 64-char string>
CLIENT_URL=https://app.yourdomain.com
```
All of these go in `.env` only. They are set in the hosting provider's
dashboard (Kloudbean) for production — never uploaded as a file.

## Centralized Error Handling

**Every controller must follow this pattern:**
```javascript
export const getInvoices = async (req, res, next) => {
  try {
    const invoices = await Invoice.find({ userId: req.user.id });
    res.status(200).json({ success: true, data: invoices });
  } catch (err) {
    next(err);  // passes to global error middleware — never call res.json in catch
  }
};
```

**Global error middleware (registered last in app.js):**
```javascript
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    success: false,
    errorCode: err.code || "SERVER_ERROR",
    message: err.message || "An unexpected error occurred.",
  });
});
```
Why: without this, Node.js crashes silently on unhandled errors, leaving
the client with no response and the developer with no traceable cause.

## Automated Maintenance (Cron Jobs)

Use node-cron for background tasks that must run on a schedule:
- Daily trash purge: delete records soft-deleted more than 30 days ago.
- Token sweeper: delete expired password reset tokens from the database.

```javascript
cron.schedule("0 0 * * *", async () => {
  await Trash.deleteMany({ deletedAt: { $lt: new Date(Date.now() - 30 * 86400000) } });
});
```

## Production Readiness Checklist

Before any backend goes live, every item must be checked:
- [ ] `.env` is in `.gitignore` — confirm with `git status` before pushing
- [ ] All passwords hashed with bcrypt — no plaintext stored anywhere
- [ ] Global error middleware registered and returning JSON (not HTML errors)
- [ ] MongoDB Atlas IP whitelist contains only production server IP — not `0.0.0.0/0`
- [ ] CORS origin set to exact production domain — not `*`
- [ ] All env variables set in hosting dashboard — not uploaded as a file
- [ ] Cron jobs running for scheduled maintenance tasks
- [ ] Postman collection created and all endpoints verified locally

## Decision Guide

| Situation | Correct action |
|-----------|---------------|
| Unsure SQL vs MongoDB | Ask: complex multi-table joins needed? Yes → SQL. No → MongoDB |
| Need to update one field only | Use PATCH, not PUT |
| Frontend getting 500 errors | Add centralized error middleware — server is crashing silently |
| API works locally but fails in production | Check: CORS origin, env vars in hosting dashboard, DB IP whitelist |
| User data leaking across accounts | Add `userId` filter to all queries — missing isolation |

## Anti-Patterns

| ❌ Never do this | ✅ Do this instead |
|-----------------|------------------|
| Hardcode secrets in source files | Store all secrets in `.env` only |
| Leave DB open to `0.0.0.0/0` | Whitelist production server IP only |
| Use `*` as CORS origin | Set exact domain via `process.env.CLIENT_URL` |
| Store plaintext passwords | Always hash with bcrypt before storing |
| Mix logic across MVC layers | One layer, one job — strictly separated |
| Return raw error stack traces | Use centralized middleware returning structured JSON |
| Poll for payment events | Use webhooks — trigger-driven, real-time, no overhead |

## Gotchas

- A backend that works perfectly locally but crashes in production is almost
  always missing environment variables in the hosting dashboard. Check this
  first before debugging anything else.

- MongoDB Mongoose validation only runs on `save()` and `create()` — not on
  `findOneAndUpdate()` unless you pass `{ runValidators: true }`. Missing
  this allows corrupt data to bypass schema constraints.

- HttpOnly cookies require `credentials: true` in both the CORS config and
  the frontend fetch call. Missing either side and the cookie is never sent.

- JWT `process.env.JWT_ACCESS_SECRET` that is undefined will cause the
  `jwt.sign()` call to fail silently or sign with `"undefined"` as the secret —
  always check env vars load before the app starts.

---

**Handoff**
```
Skill: backend-architecture
Type: Reference
Trigger phrases covered: "build an API route", "set up auth", "JWT refresh token",
  "MongoDB schema", "backend is crashing", "CORS error"
DO NOT TRIGGER for: frontend components, CSS, React state, UI layout tasks
Test prompts to verify:
  1. "Set up JWT authentication with refresh tokens" → Should trigger; HttpOnly cookie
     pattern, access token in React state, bcrypt hashing — all enforced
  2. "My backend is returning 500 errors in production but works locally"
     → Should trigger; env vars in hosting dashboard checked, CORS origin verified
  3. "Build me a Tailwind card component" → Should NOT trigger; frontend UI task
Pass/fail rubric score: 5/5
Suggested next iteration: add SQL/PostgreSQL specific schema and query patterns
  if user's projects shift toward relational data models
```
