Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

<<<<<<< HEAD Building a Secure User Authentication API with Node.js, Express, and MongoDB: A Complete Guide

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. ​

Setting Up the Foundation Module Imports javascript

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.

Setting Up the Foundation

Module Imports

>>>>>>> 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.

Initializing the Server

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

>>>>>>> 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.

Defining the Data Model

User Schema

>>>>>>> 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
=======

Creating the Model

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

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

>>>>>>> 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.

The Registration Endpoint

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:

  1. Check for duplicates: Query the database to ensure the email isn't already registered
  2. Hash the password: Use bcrypt.hash() with 10 salt rounds to securely hash the password (never store plain text passwords!)
  3. Create user instance: Build a new User object with the submitted data
  4. Save to database: Persist the user with await newUser.save()
  5. 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

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 payloadThe 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:

  1. Find the user: Search for a user with the provided email
  2. Verify existence: Return a generic error if not found (don't reveal which part failed for security)
  3. Compare passwords: Use bcrypt.compare() to check if the submitted password matches the stored hash
  4. 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

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.

Starting the Server

>>>>>>> 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.

Understanding the Authentication Flow

Let's put it all together:

Registration Flow

  1. Client sends POST request to /api/register with username, email, and password
  2. Server checks if email exists
  3. Password is hashed using bcrypt
  4. New user is saved to MongoDB
  5. Server responds with success message

Login Flow

  1. Client sends POST request to /api/login with email and password
  2. Server finds user by email
  3. Password is verified using bcrypt comparison
  4. JWT token is generated and returned
  5. Client stores token (typically in localStorage or httpOnly cookie)

Accessing Protected Routes

  1. Client sends GET request to /api/user with JWT in Authorization header
  2. verifyToken middleware validates the token
  3. Route handler fetches user data using decoded token information
  4. 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

>>>>>>> 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
=======

Login

>>>>>>> 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 user profile (include the token from login response)

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

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!

About

A simple API using Express, MongoDB and NodeJS

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages