AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Backend Architecture

skill-alyanhaider-system-architecture-backend-architecture · by alyanhaider

A Claude skill from alyanhaider/system-architecture.

No reviews yet
0 installs
30 views
0.0% view→install

Install

$ agentstack add skill-alyanhaider-system-architecture-backend-architecture

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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 No
  • 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-alyanhaider-system-architecture-backend-architecture)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Backend Architecture? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Detailed rules for building a clean, maintainable backend — covering the routes/controllers/services/models layer pattern, third-party service integration structure, environment variable management, middleware design, and how to tell Cursor to build each layer correctly. Use this skill whenever the user is building a backend, adding an API endpoint, integrating a third-party service (Stripe, OpenAI, SendGrid, Cloudinary, etc.), asking how backend code should be organized, or asking why their backend is getting messy. Also trigger for phrases like "add an endpoint", "connect to Stripe/OpenAI/SendGrid", "backend is getting complicated", "how do I structure my API", or "where does this backend code go".Backend Architecture Skill The Fundamental Rule Every backend file has exactly one job. The moment a file does two jobs, bugs have a place to hide and changes start breaking things.

The Layer Pattern Layer Definitions routes/ → Map URL + HTTP method to a controller function controllers/ → Read the request, call a service, send the response services/ → All business logic — no HTTP objects allowed here models/ → All database interaction — nothing else middleware/ → Functions that run before the handler (auth, validation, rate limit) utils/ → Pure helper functions — no side effects, no imports from other layers integrations/ → Third-party API wrappers — one folder per service config/ → App configuration and environment variable loading One-Way Data Flow HTTP Request ↓ Route (maps to controller) ↓ Middleware (auth, validation, rate limit) ↓ Controller (reads req, calls service, sends res) ↓ Service (business logic, calls models + integrations) ↓ Model / Integration (DB query or 3rd party API call) ↓ Response back up the chain Nothing skips a layer. Nothing flows backwards.

File-by-File Rules Routes (routes/) Only URL mapping. No logic. javascript// routes/projects.js const router = require('express').Router() const { createProject, getProject, updateProject } = require('../controllers/projectController') const { requireAuth } = require('../middleware/auth') const { checkLimit } = require('../middleware/checkLimits')

router.post('/', requireAuth, checkLimit('projects'), createProject) router.get('/:id', requireAuth, getProject) router.put('/:id', requireAuth, updateProject)

module.exports = router Controllers (controllers/) Only HTTP in / HTTP out. Never directly touch the DB. javascript// controllers/projectController.js const projectService = require('../services/projectService')

async function createProject(req, res) { try { const { name, description } = req.body const project = await projectService.createProject(req.user.id, { name, description }) res.status(201).json({ project }) } catch (err) { res.status(400).json({ error: err.message }) } }

module.exports = { createProject, getProject, updateProject } Services (services/) Business logic only. No req, no res, no HTTP objects — ever. javascript// services/projectService.js const Project = require('../models/Project') const { checkProjectLimit } = require('./limitService')

async function createProject(userId, data) { // Business rule: validate before creating if (!data.name || data.name.trim().length === 0) { throw new Error('Project name is required') }

// Business rule: check user's plan limit await checkProjectLimit(userId)

return Project.create({ userId, ...data }) }

module.exports = { createProject } Models (models/) Only database queries. Use parameterized queries always. javascript// models/Project.js const db = require('../config/database')

const Project = { async create({ userId, name, description }) { const result = await db.query( 'INSERT INTO projects (user_id, name, description) VALUES (?, ?, ?)', [userId, name, description] // ← parameterized, never concatenated ) return result.rows[0] },

async findById(id, userId) { const result = await db.query( 'SELECT * FROM projects WHERE id = ? AND user_id = ?', [id, userId] ) return result.rows[0] || null } }

module.exports = Project

Third-Party Service Integration Structure When the backend connects to many external services, put each in integrations/[service-name]/. integrations/ stripe/ stripeClient.js ← initialize Stripe once, export the instance stripeService.js ← your wrapper: createCustomer, createSubscription, etc. stripeWebhooks.js ← webhook signature verification + event handlers

openai/ openaiClient.js openaiService.js ← generateText, summarize, etc.

sendgrid/ emailClient.js emailService.js ← sendWelcomeEmail, sendPasswordReset, etc.

cloudinary/ storageClient.js storageService.js ← uploadImage, deleteImage, etc. Client File Pattern — Initialize Once javascript// integrations/stripe/stripeClient.js const Stripe = require('stripe') const { stripe: stripeKey } = require('../../config/env')

module.exports = new Stripe(stripeKey, { apiVersion: '2023-10-16' }) // Import this one instance everywhere — never new Stripe(key) in multiple places Service File Pattern — Thin Wrappers javascript// integrations/stripe/stripeService.js const stripe = require('./stripeClient')

async function createCustomer(email, userId) { return stripe.customers.create({ email, metadata: { userId } }) }

async function createSubscription(customerId, priceId) { return stripe.subscriptions.create({ customer: customerId, items: [{ price: priceId }], paymentbehavior: 'defaultincomplete', expand: ['latestinvoice.paymentintent'] }) }

module.exports = { createCustomer, createSubscription } Calling Integrations from Services (never from controllers) javascript// services/billingService.js — correct const stripeService = require('../integrations/stripe/stripeService') const User = require('../models/User')

async function upgradeUserToPro(userId, priceId) { const user = await User.findById(userId) const customer = await stripeService.createCustomer(user.email, userId) const subscription = await stripeService.createSubscription(customer.id, priceId) await User.updateSubscription(userId, subscription.id) return subscription }

Environment Variables — The Config Pattern Never scatter process.env.ANYTHING across 20 files. All env vars load through one file. javascript// config/env.js const required = [ 'DATABASEURL', 'JWTSECRET', 'STRIPESECRETKEY', 'STRIPEWEBHOOKSECRET', 'OPENAIAPIKEY', 'SENDGRIDAPIKEY' ]

// App won't start if any secret is missing — no silent failures required.forEach(key => { if (!process.env[key]) { throw new Error(Missing required environment variable: ${key}) } })

module.exports = { db: process.env.DATABASEURL, jwt: process.env.JWTSECRET, stripe: process.env.STRIPESECRETKEY, stripeWebhook: process.env.STRIPEWEBHOOKSECRET, openai: process.env.OPENAIAPIKEY, email: process.env.SENDGRIDAPIKEY, port: process.env.PORT || 3000, nodeEnv: process.env.NODEENV || 'development' } Then import from config everywhere: javascriptconst { stripe, openai } = require('../config/env') // Never: process.env.STRIPESECRET_KEY directly in a service file

Middleware Patterns Auth Middleware javascript// middleware/auth.js const jwt = require('jsonwebtoken') const { jwt: jwtSecret } = require('../config/env') const User = require('../models/User')

async function requireAuth(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', '') if (!token) return res.status(401).json({ error: 'Authentication required' })

try { const decoded = jwt.verify(token, jwtSecret) req.user = await User.findById(decoded.userId) if (!req.user) return res.status(401).json({ error: 'User not found' }) next() } catch { res.status(401).json({ error: 'Invalid token' }) } }

module.exports = { requireAuth } Rate Limiting — Per Route, Not Global Different routes need different limits. OpenAI routes burn money if abused. javascript// middleware/rateLimit.js const rateLimit = require('express-rate-limit')

const openaiLimit = rateLimit({ windowMs: 60000, max: 10 }) // 10/min const authLimit = rateLimit({ windowMs: 60000, max: 5 }) // 5/min const generalLimit = rateLimit({ windowMs: 60_000, max: 100 }) // 100/min

// Applied per route in routes files: // router.post('/ai/generate', openaiLimit, aiController) // router.post('/auth/login', authLimit, authController) // router.get('/projects', generalLimit, projectController)

module.exports = { openaiLimit, authLimit, generalLimit }

Cursor Prompts for Each Layer When adding a new feature, prompt Cursor one layer at a time: Step 1 — Route:

"In routes/projects.js, add a DELETE /:id route that calls deleteProject from the project controller. Apply the requireAuth middleware."

Step 2 — Controller:

"In controllers/projectController.js, add a deleteProject function. It should get the project ID from req.params.id and the user ID from req.user.id, call projectService.deleteProject, and return 200 with { deleted: true }. No business logic here."

Step 3 — Service:

"In services/projectService.js, add deleteProject(userId, projectId). It should verify the project belongs to this user (throw 403 if not), then call Project.delete(projectId). All business logic lives here."

Step 4 — Model:

"In models/Project.js, add a delete(projectId) method that runs a parameterized DELETE query. Never concatenate the ID into the query string."

The Server Entry Files server.js — Only starts the server javascript// server.js require('dotenv').config() const app = require('./app') const { port } = require('./config/env')

app.listen(port, () => { console.log(Server running on port ${port}) }) app.js — Only wires things together javascript// app.js const express = require('express') const app = express()

app.use(express.json()) app.use('/api/projects', require('./routes/projects')) app.use('/api/auth', require('./routes/auth')) app.use('/api/billing', require('./routes/billing'))

// Webhook routes use raw body — must be before express.json() app.use('/webhooks/stripe', require('./integrations/stripe/stripeWebhooks'))

module.exports = app

Checklist Before Adding Any Endpoint

Route file only maps URL to controller — no logic Controller only reads req and sends res — calls service, nothing else Service contains all business logic — no HTTP objects Model uses parameterized queries — no string concatenation Third-party calls go through integrations/ — not called from controllers directly New env vars added to config/env.js required list Rate limiting applied at the route level Auth middleware applied where needed

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

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

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.