How to Scaffold Express.js & MongoDB REST APIs Using AI Prompt Architect
Express.js is the most popular Node.js framework, but its unopinionated nature means AI assistants generate wildly different patterns in every conversation. One session gives you callback-style error handling, the next uses async/await, and the third mixes both. A Master Prompt locks in your architectural decisions once and enforces them forever.
The Express Consistency Problem
Without a Master Prompt, AI-generated Express code has no memory of your decisions:
// ❌ Session 1: Callbacks
app.get('/users', (req, res) => {
User.find({}, (err, users) => {
if (err) return res.status(500).json({ error: err.message });
res.json(users);
});
});
// ❌ Session 2: Async/await, different error pattern
app.get('/users', async (req, res) => {
try {
const users = await User.find();
res.json({ data: users, count: users.length });
} catch (e) {
res.status(500).send('Server error');
}
});
Step 1: Define Your Express Architecture
framework: Express.js 4.x
language: TypeScript (strict mode)
database: MongoDB with Mongoose ODM
project_structure:
src/
routes/ # Route definitions (thin — delegate to controllers)
controllers/ # Business logic
models/ # Mongoose schemas + models
middleware/ # Auth, validation, error handling
services/ # External integrations
utils/ # Shared helpers
config/ # Environment config
types/ # TypeScript interfaces
conventions:
- Async/await everywhere — no callbacks
- Centralised error handler middleware
- Joi/Zod validation on every route
- Consistent JSON response shape: { data, meta, error }
- HTTP status codes follow REST standards strictly
Step 2: Generated Patterns
The Master Prompt produces consistent controllers:
// controllers/users.controller.ts — Generated pattern
import { Request, Response, NextFunction } from 'express';
import { User } from '../models/user.model';
import { AppError } from '../utils/appError';
import { createUserSchema } from '../validators/user.validator';
export const getUsers = async (req: Request, res: Response, next: NextFunction) => {
try {
const { page = 1, limit = 20 } = req.query;
const users = await User.find()
.select('-password')
.skip((+page - 1) * +limit)
.limit(+limit)
.lean();
const total = await User.countDocuments();
res.json({
data: users,
meta: { page: +page, limit: +limit, total, pages: Math.ceil(total / +limit) }
});
} catch (error) {
next(error);
}
};
export const createUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const validated = createUserSchema.parse(req.body);
const user = await User.create(validated);
res.status(201).json({ data: user });
} catch (error) {
next(error);
}
};
Step 3: Error Handling Middleware
// middleware/errorHandler.ts
export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
const status = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal server error';
res.status(status).json({
error: { message, code: err.code || 'INTERNAL_ERROR' },
data: null
});
};
Key Takeaways
- Thin routes, fat controllers — routes only define method + path + middleware chain
- Centralised error handling — every controller calls
next(error), never sends errors directly - Consistent response shape — every endpoint returns
{ data, meta, error } - Validation first — Zod/Joi schema validation before any business logic executes
- TypeScript strict mode — catches type mismatches at compile time
This backend is ideal for powering a Next.js frontend. Learn how to lock your AI into App Router best practices with our Ultimate Next.js & React Master Prompt guide.
Ready to build better prompts?
Start using AI Prompt Architect for free today.
Get Started Free