Module 5 · Testing, security and shipping Lesson 14 of 15
Security basics every full-stack app needs
A handful of habits cover most of the risk: validate everything the client sends, never trust the client for permissions, set a few safe headers, and rate-limit the login.
The concept
Never build a query by joining strings - parameterised queries are what stop injection, and they cost nothing to use from the start.
Return the smallest error you can: a message that says whether an email exists helps an attacker and helps nobody else. "Invalid email or password" for both cases is the safe answer.
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
app.use(helmet()); // sensible security headers
const loginLimit = rateLimit({ windowMs: 60_000, max: 5 });
app.post('/api/login', loginLimit, loginHandler);
// parameterised query - never string-joined
const rows = await db.query(
'SELECT id, password_hash FROM users WHERE email = ?',
[email]
);Worked example
A rate limit of five login attempts a minute turns a password-guessing script into a slow, detectable trickle instead of a crash test.
Your practice task
Add input validation, a rate limit on login and three security headers to your API, then note what each one prevents.
Do this before moving on. Reading a solution teaches the shape of it; typing it teaches you.
Key takeaway
Never build a query by joining strings - parameterised queries are what stop injection, and they cost nothing to use from the start.
Keep your place in this course
A free account saves which lessons you have finished and unlocks the certificate. The lesson itself stays free to read.
Create free account or sign inCourse complete
You finished every lesson.
Your certificate is ready, and the projects you built are the proof to attach to an application.
Almost there
15 lessons left of 15
Finish every lesson to unlock the certificate. Each one ends in a practice task — do them and the final project brief writes itself.
