<<<<<<< HEAD Building a Secure User Authentication API with Node.js, Express, and MongoDB: A Complete Guide
f130d0f (docs: add comprehensive authentication guide with code breakdown) Authentication is the backbone of modern web applications. Whether you're building a social media platform, an e-commerce site, or a SaaS product, you need a robust system to register users, verify their credentials, and protect sensitive routes. In this comprehensive guide, I'll walk you through a complete authentication system built with Node.js, Express, MongoDB, and JWT tokens.
By the end of this article, you'll understand every line of code and be able to implement this pattern in your own projects. Let's dive in!
<<<<<<< HEAD What We're Building We'll create a RESTful API with three main features:
User registration with password hashing
User login with JWT token generation
Protected routes that require authentication
The Tech Stack Before we start, let's understand our tools:
Express.js: A minimal web framework for Node.js that simplifies routing and middleware management.
MongoDB + Mongoose: MongoDB is our NoSQL database, and Mongoose is an ODM (Object Data Modeling) library that provides a schema-based solution to model our data.
bcryptjs: A password hashing library that protects user passwords using one-way encryption with salt rounds.
jsonwebtoken: Enables stateless authentication by creating and verifying JWT tokens that clients send with each request.
We'll create a RESTful API with three main features:
- User registration with password hashing
- User login with JWT token generation
- Protected routes that require authentication
Before we start, let's understand our tools:
Express.js: A minimal web framework for Node.js that simplifies routing and middleware management.
MongoDB + Mongoose: MongoDB is our NoSQL database, and Mongoose is an ODM (Object Data Modeling) library that provides a schema-based solution to model our data.
bcryptjs: A password hashing library that protects user passwords using one-way encryption with salt rounds.
jsonwebtoken: Enables stateless authentication by creating and verifying JWT tokens that clients send with each request.
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
<<<<<<< HEAD
Every Express application starts with importing dependencies. Express handles our HTTP server, Mongoose manages database operations, bcryptjs secures passwords, and jsonwebtoken handles authentication tokens.
Initializing the Server
javascript
const app = express();
const PORT = 3000;
We create our Express application instance and define the port. In production, you'd typically use process.env.PORT to allow dynamic port assignment.
Connecting to MongoDB
javascript
=======Every Express application starts with importing dependencies. Express handles our HTTP server, Mongoose manages database operations, bcryptjs secures passwords, and jsonwebtoken handles authentication tokens.
const app = express();
const PORT = 3000;We create our Express application instance and define the port. In production, you'd typically use process.env.PORT to allow dynamic port assignment.
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
mongoose.connect('mongodb://localhost:27017/mydatabase')
.then(() => {
console.log('Connected to MongoDB');
})
.catch((error) => {
console.error('Error connecting to MongoDB:', error);
});
<<<<<<< HEAD
This establishes our database connection. The connection string format is mongodb://host:port/database. The promise-based approach with .then() and .catch() handles connection success and failure gracefully.
Pro tip: In production, store your connection string in environment variables: process.env.MONGODB_URI.
Defining the Data Model
User Schema
javascript
=======This establishes our database connection. The connection string format is mongodb://host:port/database. The promise-based approach with .then() and .catch() handles connection success and failure gracefully.
Pro tip: In production, store your connection string in environment variables: process.env.MONGODB_URI.
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const userSchema = new mongoose.Schema({
username: String,
email: String,
password: String
});
<<<<<<< HEAD
A schema defines the structure of documents in your MongoDB collection. Think of it as a blueprint for your data. Here we define three fields, all of type String.
For production applications, add validation:
javascript
=======A schema defines the structure of documents in your MongoDB collection. Think of it as a blueprint for your data. Here we define three fields, all of type String.
For production applications, add validation:
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true, minlength: 8 }
});
<<<<<<< HEAD
Creating the Model
javascript
const User = mongoose.model('User', userSchema);
The model is a constructor that creates and reads documents from MongoDB. Mongoose automatically pluralizes "User" to create a "users" collection in your database.
Essential Middleware
JSON Body Parser
javascript
app.use(express.json());
This middleware parses incoming JSON request bodies and makes the data accessible via req.body. Without this, you can't read JSON data from POST requests.
JWT Verification Middleware
javascript
=======const User = mongoose.model('User', userSchema);The model is a constructor that creates and reads documents from MongoDB. Mongoose automatically pluralizes "User" to create a "users" collection in your database.
app.use(express.json());This middleware parses incoming JSON request bodies and makes the data accessible via req.body. Without this, you can't read JSON data from POST requests.
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const verifyToken = (req, res, next) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
jwt.verify(token, 'secret', (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Unauthorized' });
}
req.user = decoded;
next();
});
};
<<<<<<< HEAD
This custom middleware protects routes by validating JWT tokens. Here's how it works:
Extracts the token from the Authorization header
Returns 401 (Unauthorized) if no token is present
Verifies the token signature using the secret key
Attaches decoded user data to req.user
Calls next() to proceed to the route handler
Security warning: Replace 'secret' with a strong secret stored in process.env.JWT_SECRET.
The Registration Endpoint
javascript
app.post('/api/register', async (req, res) => {
try {
=======This custom middleware protects routes by validating JWT tokens. Here's how it works:
- Extracts the token from the Authorization header
- Returns 401 (Unauthorized) if no token is present
- Verifies the token signature using the secret key
- Attaches decoded user data to
req.user - Calls
next()to proceed to the route handler
Security warning: Replace 'secret' with a strong secret stored in process.env.JWT_SECRET.
app.post('/api/register', async (req, res) => {
try {
// Check if the email already exists
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const existingUser = await User.findOne({ email: req.body.email });
if (existingUser) {
return res.status(400).json({ error: 'Email already exists' });
}
<<<<<<< HEAD
const hashedPassword = await bcrypt.hash(req.body.password, 10);
=======
// Hash the password
const hashedPassword = await bcrypt.hash(req.body.password, 10);
// Create a new user
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const newUser = new User({
username: req.body.username,
email: req.body.email,
password: hashedPassword
});
await newUser.save();
res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
<<<<<<< HEAD
The registration flow follows this sequence:
Check for duplicates: Query the database to ensure the email isn't already registered
Hash the password: Use bcrypt.hash() with 10 salt rounds to securely hash the password (never store plain text passwords!)
Create user instance: Build a new User object with the submitted data
Save to database: Persist the user with await newUser.save()
Return success: Send a 201 (Created) status with confirmation message
The try-catch block ensures any errors (validation failures, database issues) return a 500 status code.
The Login Endpoint
javascript
app.post('/api/login', async (req, res) => {
try {
=======The registration flow follows this sequence:
- Check for duplicates: Query the database to ensure the email isn't already registered
- Hash the password: Use
bcrypt.hash()with 10 salt rounds to securely hash the password (never store plain text passwords!) - Create user instance: Build a new User object with the submitted data
- Save to database: Persist the user with
await newUser.save() - Return success: Send a 201 (Created) status with confirmation message
The try-catch block ensures any errors (validation failures, database issues) return a 500 status code.
app.post('/api/login', async (req, res) => {
try {
// Check if the email exists
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const user = await User.findOne({ email: req.body.email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
<<<<<<< HEAD
=======
// Compare passwords
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const passwordMatch = await bcrypt.compare(req.body.password, user.password);
if (!passwordMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
<<<<<<< HEAD
=======
// Generate JWT token
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const token = jwt.sign({ email: user.email }, 'secret');
res.status(200).json({ token });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
<<<<<<< HEAD
Authentication happens in four steps:
Find the user: Search for a user with the provided email
Verify existence: Return a generic error if not found (don't reveal which part failed for security)
Compare passwords: Use bcrypt.compare() to check if the submitted password matches the stored hash
Generate JWT: Create a token containing the user's email as payload
The client receives this token and includes it in the Authorization header for future requests.
Protected Routes
javascript
app.get('/api/user', verifyToken, async (req, res) => {
try {
=======Authentication happens in four steps:
- Find the user: Search for a user with the provided email
- Verify existence: Return a generic error if not found (don't reveal which part failed for security)
- Compare passwords: Use
bcrypt.compare()to check if the submitted password matches the stored hash - Generate JWT: Create a token containing the user's email as payload
The client receives this token and includes it in the Authorization header for future requests.
app.get('/api/user', verifyToken, async (req, res) => {
try {
// Fetch user details using decoded token
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
const user = await User.findOne({ email: req.user.email });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(200).json({ username: user.username, email: user.email });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
<<<<<<< HEAD
This demonstrates the protected route pattern. The verifyToken middleware runs first, validating the JWT before the route handler executes. The decoded token data (req.user) identifies which user is making the request.
Notice we never return the password field—sensitive data should never be sent to clients.
Starting the Server
javascript
=======This demonstrates the protected route pattern. The verifyToken middleware runs first, validating the JWT before the route handler executes. The decoded token data (req.user) identifies which user is making the request.
Notice we never return the password field—sensitive data should never be sent to clients.
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
app.get('/', (req, res) => {
res.send('Welcome to my User Registration and Login API!');
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
<<<<<<< HEAD
The default route provides a simple welcome message. The app.listen() method starts the HTTP server on the specified port, and the callback confirms the server is running.
Understanding the Authentication Flow
Let's put it all together:
Registration Flow
Client sends POST request to /api/register with username, email, and password
Server checks if email exists
Password is hashed using bcrypt
New user is saved to MongoDB
Server responds with success message
Login Flow
Client sends POST request to /api/login with email and password
Server finds user by email
Password is verified using bcrypt comparison
JWT token is generated and returned
Client stores token (typically in localStorage or httpOnly cookie)
Accessing Protected Routes
Client sends GET request to /api/user with JWT in Authorization header
verifyToken middleware validates the token
Route handler fetches user data using decoded token information
Server returns user profile
HTTP Status Codes Explained
Understanding status codes improves your API design:
200 (OK): Successful GET, PUT, or PATCH request
201 (Created): Successful POST request that created a resource
400 (Bad Request): Client sent invalid data
401 (Unauthorized): Missing or invalid authentication
404 (Not Found): Resource doesn't exist
500 (Internal Server Error): Server-side error
Security Best Practices
While this code provides a solid foundation, production applications need additional security measures:
Environment variables: Store secrets in .env files, never hardcode them
Token expiration: Add expiration to JWT tokens: jwt.sign(payload, secret, { expiresIn: '1h' })
Refresh tokens: Implement refresh token mechanism for long-lived sessions
Rate limiting: Prevent brute force attacks with libraries like express-rate-limit
HTTPS: Always use HTTPS in production to encrypt data in transit
Input validation: Use libraries like Joi or express-validator to sanitize inputs
Password requirements: Enforce strong password policies
Reusable Patterns for Your Projects
This code demonstrates several patterns you'll use repeatedly:
Middleware chain: Request → express.json() → verifyToken → Route Handler → Response
Async/await with try-catch: Always wrap database operations to handle errors gracefully
Model-based operations: Use Mongoose models for all database interactions (.find(), .findOne(), .save())
Response consistency: Return JSON objects with consistent structure
Testing Your API
You can test this API using Postman, cURL, or any HTTP client:
Register a user:
text
=======The default route provides a simple welcome message. The app.listen() method starts the HTTP server on the specified port, and the callback confirms the server is running.
Let's put it all together:
- Client sends POST request to
/api/registerwith username, email, and password - Server checks if email exists
- Password is hashed using bcrypt
- New user is saved to MongoDB
- Server responds with success message
- Client sends POST request to
/api/loginwith email and password - Server finds user by email
- Password is verified using bcrypt comparison
- JWT token is generated and returned
- Client stores token (typically in localStorage or httpOnly cookie)
- Client sends GET request to
/api/userwith JWT in Authorization header verifyTokenmiddleware validates the token- Route handler fetches user data using decoded token information
- Server returns user profile
Understanding status codes improves your API design:
- 200 (OK): Successful GET, PUT, or PATCH request
- 201 (Created): Successful POST request that created a resource
- 400 (Bad Request): Client sent invalid data
- 401 (Unauthorized): Missing or invalid authentication
- 404 (Not Found): Resource doesn't exist
- 500 (Internal Server Error): Server-side error
While this code provides a solid foundation, production applications need additional security measures:
- Environment variables: Store secrets in
.envfiles, never hardcode them - Token expiration: Add expiration to JWT tokens:
jwt.sign(payload, secret, { expiresIn: '1h' }) - Refresh tokens: Implement refresh token mechanism for long-lived sessions
- Rate limiting: Prevent brute force attacks with libraries like
express-rate-limit - HTTPS: Always use HTTPS in production to encrypt data in transit
- Input validation: Use libraries like Joi or express-validator to sanitize inputs
- Password requirements: Enforce strong password policies
This code demonstrates several patterns you'll use repeatedly:
- Middleware chain: Request →
express.json()→verifyToken→ Route Handler → Response - Async/await with try-catch: Always wrap database operations to handle errors gracefully
- Model-based operations: Use Mongoose models for all database interactions (
.find(),.findOne(),.save()) - Response consistency: Return JSON objects with consistent structure
You can test this API using Postman, cURL, or any HTTP client:
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
POST http://localhost:3000/api/register
Content-Type: application/json
{
"username": "johndoe",
"email": "john@example.com",
"password": "securepass123"
}
<<<<<<< HEAD
Login:
text
=======
>>>>>>> f130d0f (docs: add comprehensive authentication guide with code breakdown)
POST http://localhost:3000/api/login
Content-Type: application/json
{
"email": "john@example.com",
"password": "securepass123"
}
<<<<<<< HEAD
Get user profile (include the token from login response):
text
GET http://localhost:3000/api/user
Authorization: your-jwt-token-here
Next Steps and Further Learning
Now that you understand this authentication system, you can:
Add password reset functionality via email
Implement OAuth2 for social login (Google, GitHub, etc.)
Create role-based access control (RBAC) for admin/user roles
Add profile update endpoints
Implement two-factor authentication (2FA)
Build a frontend application that consumes this API
Conclusion
=======
GET http://localhost:3000/api/user
Authorization: your-jwt-token-here
Now that you understand this authentication system, you can:
- Add password reset functionality via email
- Implement OAuth2 for social login (Google, GitHub, etc.)
- Create role-based access control (RBAC) for admin/user roles
- Add profile update endpoints
- Implement two-factor authentication (2FA)
- Build a frontend application that consumes this API
f130d0f (docs: add comprehensive authentication guide with code breakdown) You've now learned how to build a complete authentication system from scratch. This pattern forms the foundation of countless web applications, and understanding each component—from password hashing to JWT verification—makes you a more capable developer.
The beauty of this architecture is its modularity. You can extract the authentication logic into separate modules, add new features incrementally, and scale as your application grows.
Remember: security is not a feature you add at the end—it's baked into every decision, from choosing bcrypt for password hashing to using JWT for stateless authentication.
<<<<<<< HEAD Ready to implement this in your project? Start by setting up MongoDB locally, install the dependencies (npm install express mongoose bcryptjs jsonwebtoken), and test each endpoint. Happy coding!
Ready to implement this in your project? Start by setting up MongoDB locally, install the dependencies (npm install express mongoose bcryptjs jsonwebtoken), and test each endpoint. Happy coding!
f130d0f (docs: add comprehensive authentication guide with code breakdown)
Have questions or want to see this pattern implemented in other languages like Python, Ruby, Java, PHP, or C#? Drop a comment below!