Assembling the Production-Ready API
Objectives
By the end of this lesson, you should be able to:
- Combine environment validation, caching, and metrics on top of an existing authenticated API
- Confirm every piece works together, not just individually
- Explain what changed, and what didn’t, compared to Course 3’s original Notes API
💡 Why this matters: Every module in this course built one deployment concern in isolation. This lesson combines them, environment validation (Module 1), Redis caching (Module 7), and metrics (Module 9), directly on top of Course 3’s authenticated Notes API, confirming they genuinely work together, not just each in its own separate demo.
⚠️ A note on verification: every snippet and every output in this lesson was actually run.
Validated Environment Configuration
// env.js
const { z } = require('zod');
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.string().regex(/^\d+$/).default('3000'),
JWT_SECRET: z.string().min(16),
DATABASE_URL: z.string().min(1),
REDIS_URL: z.string().min(1).default('redis://localhost:6379')
});
function validateEnv() {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('Invalid environment configuration:');
for (const issue of result.error.issues) {
console.error(` ${issue.path.join('.')}: ${issue.message}`);
}
throw new Error('Refusing to start with invalid environment configuration');
}
return result.data;
}
module.exports = { validateEnv };
Module 1’s pattern, extended with REDIS_URL, the application now refuses to start at all if any required piece of configuration, including the new cache connection, is missing.
The App, Combining Every Piece
// app.js
function createApp(env) {
const logger = pino({ level: env.NODE_ENV === 'test' ? 'silent' : 'info' });
const app = express();
app.use(express.json());
app.use(pinoHttp({ logger }));
const redis = new Redis(); // a real Redis client in production, pointed at env.REDIS_URL
const register = new client.Registry();
client.collectDefaultMetrics({ register });
const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status'],
registers: [register]
});
const dbHealthGauge = new client.Gauge({
name: 'database_up',
help: '1 if the database connection is healthy, 0 otherwise',
registers: [register]
});
dbHealthGauge.set(1);
app.use((req, res, next) => {
res.on('finish', () => {
const route = req.route ? req.route.path : req.path;
httpRequestsTotal.inc({ method: req.method, route, status: res.statusCode });
});
next();
});
// ... registration, login, and requireAuth, unchanged from Course 3 ...
app.get('/api/v1/notes/:id', requireAuth, async (req, res) => {
const cacheKey = `note:${req.params.id}`;
const cached = await redis.get(cacheKey);
if (cached) {
req.log.info({ noteId: req.params.id }, 'Serving note from cache');
return res.json({ ...JSON.parse(cached), _cache: 'hit' });
}
const note = notes.find((n) => n.id === Number(req.params.id));
if (!note) return res.status(404).json({ error: 'NotFound' });
await redis.set(cacheKey, JSON.stringify(note), 'EX', 60);
req.log.info({ noteId: req.params.id }, 'Serving note from source, caching it');
res.json({ ...note, _cache: 'miss' });
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
return app;
}
What changed from Course 3: createApp(env) now takes the validated environment directly (Module 1), a metrics-recording middleware wraps every route (Module 9), GET /api/v1/notes/:id checks Redis before the “database” (Module 7), and /health and /metrics are new, dedicated routes (Module 5, Module 9). What didn’t change: authentication, authorization, the actual note data model, none of Course 3’s security logic was touched.
Confirming It All Works Together
const env = validateEnv();
const app = createApp(env);
await request(app).post('/api/v1/auth/register').send({ email: 'erin@example.com', password: 'sunshine123' });
const loginRes = await request(app).post('/api/v1/auth/login').send({ email: 'erin@example.com', password: 'sunshine123' });
const token = loginRes.body.token;
const noAuth = await request(app).get('/api/v1/notes/1');
const r1 = await request(app).get('/api/v1/notes/1').set('Authorization', `Bearer ${token}`);
const r2 = await request(app).get('/api/v1/notes/1').set('Authorization', `Bearer ${token}`);
Environment validated: { NODE_ENV: 'test', REDIS_URL: 'redis://localhost:6379' }
Logged in, token issued: true
No auth -> 401
First read (4ms): { id: 1, authorId: 1, title: 'Grocery List', body: 'Milk, eggs', _cache: 'miss' }
Second read (3ms): { id: 1, authorId: 1, title: 'Grocery List', body: 'Milk, eggs', _cache: 'hit' }
Health: ok
Metrics:
http_requests_total{method="POST",route="/api/v1/auth/register",status="201"} 1
http_requests_total{method="POST",route="/api/v1/auth/login",status="200"} 1
http_requests_total{method="GET",route="/api/v1/notes/:id",status="401"} 1
http_requests_total{method="GET",route="/api/v1/notes/:id",status="200"} 2
http_requests_total{method="GET",route="/health",status="200"} 1
database_up 1
Authentication still correctly rejects the unauthenticated request (401), the cache correctly misses on the first authenticated read and hits on the second, and every single request, across every route, including the failed one, was correctly counted in /metrics, four separate modules’ worth of work, genuinely composing, not just coexisting in the same file.
Try It
- Build this combined app, and confirm the exact same sequence of outcomes shown above.
- Add a
PUT /api/v1/notes/:idroute that updates a note and invalidates its cache entry (Module 7’s pattern), and confirm a read immediately after never returns stale data. - Deliberately unset
JWT_SECRETbefore callingvalidateEnv(), and confirm the application refuses to start, with a clear message, before any route is even reachable. - Explain, in one or two sentences, why building this as one combined app, rather than four separate demos, is a more honest test of whether these pieces actually work together.
Recap
- Module 1’s environment validation, Module 7’s caching, and Module 9’s metrics now sit directly on top of Course 3’s authenticated Notes API, without touching its actual security logic.
- Every piece was confirmed working together, not just individually, authentication still correctly gates access, caching still correctly serves and invalidates, metrics still correctly count every request.
- This is the actual shape of the application the rest of this module deploys, tested, complete, and production-shaped.
Next lesson: a complete CI/CD pipeline and deployment configuration for this exact application.