10 Expert Prompts for Node.js and Express: API, Middleware, and Authorization
Building robust Node.js applications with Express is a skill that many developers master through years of trial and error. But what if you could accelerate that learning curve by leveraging AI-powered prompts that generate production-grade code? Whether you're crafting REST APIs, implementing secure authentication, or integrating real-time features with WebSockets, the right prompt can save you hours of boilerplate and debugging.
In this article, I'll share 10 carefully designed prompts for Node.js and Express, organized by complexity: basic, advanced, and expert. Each prompt includes a clear task, the exact prompt text you can copy, and a concrete example of the generated output. These prompts are tested against modern best practices as of July 2026, and they rely on well-documented libraries that are actively maintained.
Why Use Prompts for Node.js Development?
Before diving into the prompts, let's clarify why this approach works. AI assistants like Claude or GPT can generate code snippets, but the quality depends heavily on how you phrase the request. A vague prompt like "write an Express app" yields generic results. A structured prompt that specifies the framework version, middleware stack, authentication method, and error handling produces code you can almost drop into a project.
According to the official Express documentation (expressjs.com), the framework has over 14 million weekly downloads on npm as of early 2026. It remains the most popular Node.js web framework because of its minimalism and extensibility. The prompts below follow Express 4.x patterns but are compatible with Express 5 (currently in release candidate).
Basic Prompts: Laying the Foundation
These prompts are ideal for beginners or for quickly scaffolding a simple API.
1. Simple REST API with CRUD Operations
Task: Generate a basic Express server with in-memory data storage and full CRUD endpoints for a resource (e.g., users).
Prompt:
Write a complete Express.js server (version 4.x) that implements a RESTful API for managing a list of users. Use an in-memory array as the data store. Include the following endpoints:
- GET /users – return all users
- GET /users/:id – return a single user by id (use numeric ids)
- POST /users – create a new user (fields: name, email)
- PUT /users/:id – update an existing user
- DELETE /users/:id – delete a user
Add basic error handling: return 404 if user not found, 400 if required fields are missing, and 500 for unexpected errors. Use express.json() middleware. Listen on port 3000.
Example Result:
const express = require('express');
const app = express();
app.use(express.json());
let users = [];
let nextId = 1;
app.get('/users', (req, res) => {
res.json(users);
});
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
const newUser = { id: nextId++, name, email };
users.push(newUser);
res.status(201).json(newUser);
});
app.put('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: 'User not found' });
const { name, email } = req.body;
if (name) user.name = name;
if (email) user.email = email;
res.json(user);
});
app.delete('/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: 'User not found' });
users.splice(index, 1);
res.status(204).send();
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
2. Custom Middleware for Logging and Timing
Task: Create a middleware that logs each request's method, URL, and processing time.
Prompt:
Write a custom Express middleware function named requestLogger that logs the HTTP method, URL, and the time taken to process each request (in milliseconds). Apply this middleware globally. Also write a second middleware that adds a custom header 'X-Processed-By' with value 'Express' to every response. Show how to include both in an Express app.
Example Result:
const express = require('express');
const app = express();
// Middleware 1: request logger with timing
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} - ${duration}ms`);
});
next();
});
// Middleware 2: custom header
app.use((req, res, next) => {
res.setHeader('X-Processed-By', 'Express');
next();
});
app.get('/', (req, res) => res.send('Hello World'));
app.listen(3000);
Advanced Prompts: Authentication and Data Persistence
These prompts move beyond in-memory storage and introduce real-world patterns.
3. JWT-Based Authentication with bcrypt
Task: Implement user registration and login using JSON Web Tokens (JWT) and password hashing with bcrypt.
Prompt:
Write an Express.js application with user authentication using JWT (jsonwebtoken) and bcrypt for password hashing. Include:
- POST /register – accepts { username, password }, hashes password, stores in an array (in-memory)
- POST /login – validates credentials, returns a JWT token with 1-hour expiry
- GET /protected – a protected route that verifies the JWT token from the Authorization header (Bearer scheme) and returns the username from the token payload
Use environment variables for JWT_SECRET and PORT. Include error handling for invalid credentials and missing tokens.
Example Result:
require('dotenv').config();
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const users = []; // In-memory store, replace with DB in production
app.post('/register', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) return res.status(400).json({ error: 'Username and password required' });
const hashedPassword = await bcrypt.hash(password, 10);
users.push({ username, password: hashedPassword });
res.status(201).json({ message: 'User created' });
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });
const token = jwt.sign({ username }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
});
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Token missing' });
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
}
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: `Hello, ${req.user.username}` });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server on port ${PORT}`));
4. Role-Based Access Control (RBAC) Middleware
Task: Extend the JWT authentication with role-based authorization.
Prompt:
Extend the previous Express app to include role-based access control. When registering, accept a role field ('admin' or 'user'). The JWT payload should include the role. Create a middleware function authorize(role) that checks if the user's role matches the required role. Add two protected routes:
- GET /admin – only accessible by admin role
- GET /profile – accessible by any authenticated user
Return 403 Forbidden if the role is insufficient.
Example Result:
// ... registration modified to include role:
users.push({ username, password: hashedPassword, role: req.body.role || 'user' });
// Login now includes role in token:
const token = jwt.sign({ username, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' });
// Authorization middleware:
function authorize(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
app.get('/admin', authenticateToken, authorize('admin'), (req, res) => {
res.json({ message: 'Admin panel' });
});
app.get('/profile', authenticateToken, (req, res) => {
res.json({ user: req.user.username, role: req.user.role });
});
Expert Prompts: Real-Time, Error Handling, and Production Patterns
These prompts tackle complex scenarios that senior developers face daily.
5. WebSocket Integration with Socket.IO
Task: Add real-time bidirectional communication to an Express app using Socket.IO.
Prompt:
Write an Express server that integrates Socket.IO for real-time chat functionality. Requirements:
- Serve a simple HTML page with a chat interface
- When a user sends a message via Socket.IO, broadcast it to all connected clients
- Display a notification when a user connects or disconnects
- Use the 'connection' event and emit 'chat message' events
- Ensure robust error handling: log socket errors and handle reconnection
Example Result:
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', (socket) => {
console.log('A user connected');
io.emit('chat message', 'A user has joined the chat');
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
});
socket.on('disconnect', () => {
console.log('A user disconnected');
io.emit('chat message', 'A user has left the chat');
});
socket.on('error', (err) => {
console.error('Socket error:', err);
});
});
server.listen(3000, () => console.log('Server running on port 3000'));
6. Centralized Error Handling with Custom Error Classes
Task: Implement a structured error handling system for an Express API.
Prompt:
Create a custom error handling system for an Express.js REST API. Define a base AppError class that extends Error, with properties: statusCode, message, and isOperational. Create specific error classes: NotFoundError, ValidationError, UnauthorizedError. Then write a global error handling middleware that:
- Returns JSON with { error: message, status: statusCode }
- Logs the error stack in development mode
- For non-operational errors, returns 500 and does not leak error details
- Use environment variable NODE_ENV to toggle between development and production
Example Result:
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401);
}
}
// Global error handler
app.use((err, req, res, next) => {
if (process.env.NODE_ENV === 'development') {
console.error(err.stack);
}
if (err.isOperational) {
res.status(err.statusCode).json({ error: err.message, status: err.statusCode });
} else {
// Programming or unknown error: don't leak details
res.status(500).json({ error: 'Internal server error', status: 500 });
}
});
// Usage:
app.get('/user/:id', (req, res, next) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return next(new NotFoundError('User'));
res.json(user);
});
7. Rate Limiting with express-rate-limit
Task: Protect an API from abuse by implementing rate limiting.
Prompt:
Implement rate limiting on an Express API using the express-rate-limit package. Configure:
- A global limiter: max 100 requests per 15 minutes per IP
- A stricter limiter for the /auth endpoints: max 5 requests per minute
- Return a 429 status with a JSON error message when limit is exceeded
- Show how to set custom headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
Example Result:
const rateLimit = require('express-rate-limit');
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later.' },
});
const authLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 5,
message: { error: 'Too many authentication attempts, try again later.' },
});
app.use(globalLimiter);
app.use('/auth', authLimiter);
Real-World Use Cases and Integration
These prompts are not theoretical. Many production systems use similar patterns. For example, when building a dashboard that integrates with external services like Stripe for payments or Telegram for notifications, you can reuse the JWT and RBAC middleware from prompts 3 and 4. The same applies to logging middleware and error handling.
ASI Biont supports connecting to external APIs like Telegram and Stripe through its API integration layer — you can learn more about this at asibiont.com/courses. This allows you to extend the patterns above with real data sources.
8. File Upload with Multer
Task: Create an endpoint for file uploads with validation.
Prompt:
Write an Express route that handles file uploads using multer. Requirements:
- Accept only image files (jpeg, png, gif)
- Limit file size to 5MB
- Store files in a local 'uploads/' directory
- Return the file path after upload
- Handle errors: file too large, invalid type, no file
Example Result:
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg
|jpg|png|gif/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) return cb(null, true);
cb(new Error('Only image files are allowed'));
}
});
app.post('/upload', (req, res) => {
upload.single('image')(req, res, (err) => {
if (err) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File too large (max 5MB)' });
}
return res.status(400).json({ error: err.message });
}
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
res.json({ path: req.file.path });
});
});
9. Environment Configuration and Validation
Task: Set up a robust configuration system using dotenv and joi validation.
Prompt:
Write a configuration module for a Node.js Express app that:
- Loads environment variables from a .env file using dotenv
- Validates required variables (PORT, JWT_SECRET, DATABASE_URL) using Joi
- Provides a typed config object with defaults
- Throws a descriptive error if validation fails
Example Result:
const dotenv = require('dotenv');
const Joi = require('joi');
dotenv.config();
const envSchema = Joi.object({
PORT: Joi.number().default(3000),
JWT_SECRET: Joi.string().required(),
DATABASE_URL: Joi.string().uri().required(),
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
}).unknown();
const { error, value: envVars } = envSchema.validate(process.env);
if (error) {
throw new Error(`Config validation error: ${error.message}`);
}
const config = {
port: envVars.PORT,
jwtSecret: envVars.JWT_SECRET,
databaseUrl: envVars.DATABASE_URL,
nodeEnv: envVars.NODE_ENV,
};
module.exports = config;
10. Testing with Jest and Supertest
Task: Write integration tests for an Express API.
Prompt:
Write integration tests using Jest and Supertest for the CRUD API from prompt 1. Test:
- GET /users returns an array
- POST /users creates a user and returns 201
- GET /users/:id returns 404 for non-existent id
- DELETE /users/:id returns 204
Use beforeEach to clear the user array before each test.
Example Result:
const request = require('supertest');
const app = require('./app'); // assuming app is exported
describe('Users API', () => {
beforeEach(() => {
// Reset users array (requires exporting it or using a module)
app.locals.users = [];
});
test('GET /users returns empty array', async () => {
const res = await request(app).get('/users');
expect(res.statusCode).toBe(200);
expect(res.body).toEqual([]);
});
test('POST /users creates a user', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Alice', email: 'alice@example.com' });
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('id');
expect(res.body.name).toBe('Alice');
});
test('GET /users/:id returns 404 for invalid id', async () => {
const res = await request(app).get('/users/999');
expect(res.statusCode).toBe(404);
});
test('DELETE /users/:id returns 204', async () => {
const createRes = await request(app)
.post('/users')
.send({ name: 'Bob', email: 'bob@example.com' });
const id = createRes.body.id;
const delRes = await request(app).delete(`/users/${id}`);
expect(delRes.statusCode).toBe(204);
});
});
Summary Table of Prompts
| Prompt # | Category | Key Libraries | Use Case |
|---|---|---|---|
| 1 | Basic | express | CRUD API |
| 2 | Basic | express | Custom middleware |
| 3 | Advanced | bcrypt, jsonwebtoken | JWT auth |
| 4 | Advanced | jsonwebtoken | RBAC |
| 5 | Expert | socket.io | Real-time chat |
| 6 | Expert | express | Error handling |
| 7 | Expert | express-rate-limit | Rate limiting |
| 8 | Expert | multer | File upload |
| 9 | Expert | dotenv, joi | Config validation |
| 10 | Expert | jest, supertest | Integration testing |
Conclusion
These 10 prompts cover the essential patterns for building production-ready Node.js and Express applications. By using structured prompts, you can generate code that follows best practices, includes error handling, and is ready for deployment. The key is to be specific: include library names, error scenarios, and configuration details.
Remember that prompts are a starting point. Always review generated code for security vulnerabilities, especially when handling authentication and file uploads. As you gain experience, you can customize these prompts to match your project's architecture and coding standards.
Start with the basic prompts to warm up, then move to advanced authentication patterns, and finally tackle expert-level real-time and testing setups. Your Node.js development workflow will never be the same.
Comments