The Challenge Every Developer Faces
You’re building a new project, and you need a backend API. Fast. The clock is ticking, the coffee is getting cold, and you don’t have time to set up complex frameworks or learn new paradigms. You just need something that works, something that handles HTTP requests, talks to a database, and returns JSON.
Well, you’re in luck. In the next 10 minutes, I’m going to walk you through building a production-ready REST API using Node.js and Express. No fluff, no over-engineering—just the essentials that work.
By the end of this tutorial, you’ll have a working API with:
- ✅ CRUD operations (Create, Read, Update, Delete)
- ✅ Environment configuration
- ✅ Error handling
- ✅ Route structure
- ✅ Database integration (MongoDB)
- ✅ Authentication ready to plug in
Let’s get started. Your API is 10 minutes away.
Step 0: Prerequisites (30 seconds)
Before we dive in, make sure you have:
node --version # Should be v16 or higher
npm --version # v7 or higher
If you don’t have Node.js installed, download it here. I’ll wait.
Ready? Let’s go.
Step 1: Initialize Your Project (30 seconds)
Create a new directory and initialize a Node.js project:
mkdir my-awesome-api
cd my-awesome-api
npm init -y
The -y flag accepts all defaults, saving you precious seconds.
Step 2: Install Dependencies (45 seconds)
We need three core packages:
npm install express dotenv cors helmet morgan
npm install --save-dev nodemon
Here’s what each does:
- Express: The web framework that does the heavy lifting
- dotenv: Loads environment variables from a
.envfile - cors: Enables Cross-Origin Resource Sharing
- helmet: Adds security headers
- morgan: Logs HTTP requests
- nodemon: Auto-restarts the server on file changes (development only)
Step 3: Project Structure (30 seconds)
Create this folder structure:
mkdir src
cd src
mkdir routes controllers models middleware utils
cd ..
Your project should look like this:
my-awesome-api/
├── src/
│ ├── routes/
│ ├── controllers/
│ ├── models/
│ ├── middleware/
│ └── utils/
├── .env
├── .gitignore
├── package.json
└── server.js
Step 4: Environment Configuration (30 seconds)
Create a .env file in your root directory:
PORT=5000
NODE_ENV=development
MONGODB_URI=mongodb://localhost:27017/myapi
JWT_SECRET=your-super-secret-key-change-this
Security Note: Never commit your
.envfile! Add it to.gitignore.
Create a .gitignore file:
node_modules/
.env
.DS_Store
dist/
build/
Step 5: The Server Setup (2 minutes)
Create server.js in your root directory:
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(helmet()); // Security headers
app.use(cors()); // CORS handling
app.use(morgan('dev')); // Logging
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'success',
message: 'API is running smoothly',
timestamp: new Date().toISOString()
});
});
// Routes will go here
app.use('/api/v1', require('./src/routes'));
// 404 handler
app.use((req, res, next) => {
res.status(404).json({
status: 'error',
message: 'Route not found'
});
});
// Global error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
status: 'error',
message: err.message || 'Something went wrong!'
});
});
// Start server
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
console.log(`📊 Environment: ${process.env.NODE_ENV}`);
});
Step 6: Database Connection (1 minute)
Let’s set up MongoDB. First, install Mongoose:
npm install mongoose
Create src/config/database.js:
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`✅ MongoDB Connected: ${conn.connection.host}`);
return conn;
} catch (error) {
console.error(`❌ Error connecting to MongoDB: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
Now update your server.js to include the database connection:
const connectDB = require('./src/config/database');
// Connect to database
connectDB();
// Rest of your server code...
Quick Tip: Using MongoDB Atlas? Replace your
MONGODB_URIwith your Atlas connection string.
Step 7: Build Your First Model (1 minute)
Create src/models/User.js:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, 'Please provide a username'],
unique: true,
trim: true,
minlength: [3, 'Username must be at least 3 characters']
},
email: {
type: String,
required: [true, 'Please provide an email'],
unique: true,
lowercase: true,
match: [
/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
'Please provide a valid email'
]
},
password: {
type: String,
required: [true, 'Please provide a password'],
minlength: [6, 'Password must be at least 6 characters'],
select: false // Don't return password by default
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true // Adds createdAt and updatedAt
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Method to compare passwords
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
Step 8: Create Your Controllers (2 minutes)
Create src/controllers/userController.js:
const User = require('../models/User');
// @desc Get all users
// @route GET /api/v1/users
// @access Public
exports.getUsers = async (req, res, next) => {
try {
const users = await User.find().select('-__v');
res.status(200).json({
status: 'success',
count: users.length,
data: users
});
} catch (error) {
next(error);
}
};
// @desc Get single user
// @route GET /api/v1/users/:id
// @access Public
exports.getUser = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
status: 'error',
message: 'User not found'
});
}
res.status(200).json({
status: 'success',
data: user
});
} catch (error) {
next(error);
}
};
// @desc Create user
// @route POST /api/v1/users
// @access Public
exports.createUser = async (req, res, next) => {
try {
const { username, email, password, role } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ $or: [{ email }, { username }] });
if (existingUser) {
return res.status(400).json({
status: 'error',
message: 'User with this email or username already exists'
});
}
const user = await User.create({
username,
email,
password,
role
});
// Remove password from response
user.password = undefined;
res.status(201).json({
status: 'success',
data: user
});
} catch (error) {
next(error);
}
};
// @desc Update user
// @route PUT /api/v1/users/:id
// @access Public
exports.updateUser = async (req, res, next) => {
try {
const { username, email, role } = req.body;
const user = await User.findByIdAndUpdate(
req.params.id,
{ username, email, role },
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({
status: 'error',
message: 'User not found'
});
}
res.status(200).json({
status: 'success',
data: user
});
} catch (error) {
next(error);
}
};
// @desc Delete user
// @route DELETE /api/v1/users/:id
// @access Public
exports.deleteUser = async (req, res, next) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({
status: 'error',
message: 'User not found'
});
}
res.status(200).json({
status: 'success',
message: 'User deleted successfully'
});
} catch (error) {
next(error);
}
};
Step 9: Set Up Routes (1 minute)
Create src/routes/index.js:
const express = require('express');
const router = express.Router();
// Import route modules
const userRoutes = require('./userRoutes');
// Mount routes
router.use('/users', userRoutes);
// Add more routes here as your API grows
// router.use('/posts', require('./postRoutes'));
// router.use('/comments', require('./commentRoutes'));
module.exports = router;
Create src/routes/userRoutes.js:
const express = require('express');
const router = express.Router();
const {
getUsers,
getUser,
createUser,
updateUser,
deleteUser
} = require('../controllers/userController');
// Define routes
router.route('/')
.get(getUsers)
.post(createUser);
router.route('/:id')
.get(getUser)
.put(updateUser)
.delete(deleteUser);
module.exports = router;
Step 10: Test Your API (30 seconds)
Add a start script to your package.json:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
Now run your development server:
npm run dev
You should see:
🚀 Server running on http://localhost:5000
📊 Environment: development
✅ MongoDB Connected: localhost
Step 11: Test Your Endpoints (1 minute)
Open Postman, Thunder Client, or just use curl:
Create a user (POST)
curl -X POST http://localhost:5000/api/v1/users \
-H "Content-Type: application/json" \
-d '{"username":"johndoe","email":"john@example.com","password":"password123"}'
Get all users (GET)
curl http://localhost:5000/api/v1/users
Get a specific user (GET)
curl http://localhost:5000/api/v1/users/USER_ID_HERE
Update a user (PUT)
curl -X PUT http://localhost:5000/api/v1/users/USER_ID_HERE \
-H "Content-Type: application/json" \
-d '{"username":"johnsmith","role":"admin"}'
Delete a user (DELETE)
curl -X DELETE http://localhost:5000/api/v1/users/USER_ID_HERE
🎉 Congratulations! Your REST API is Live!
You’ve just built a fully functional REST API in under 10 minutes. Here’s what you accomplished:
- ✅ Express server with proper middleware
- ✅ MongoDB integration with Mongoose
- ✅ Full CRUD operations
- ✅ Error handling
- ✅ Environment configuration
- ✅ Clean project structure
- ✅ Production-ready setup
What’s Next? Taking It Further
Your API is ready, but there’s always room for improvement. Here are 5 enhancements to make it production-ready:
1. Add Authentication with JWT
npm install jsonwebtoken bcryptjs
2. Add Input Validation
npm install express-validator
3. Add Rate Limiting
npm install express-rate-limit
4. Add API Documentation
npm install swagger-jsdoc swagger-ui-express
5. Write Unit Tests
npm install --save-dev jest supertest
Common Issues and Solutions
Issue: “Cannot connect to MongoDB”
- Check your connection string
- Ensure MongoDB is running (
mongodin terminal) - If using Atlas, whitelist your IP address
Issue: “Port 5000 already in use”
- Kill the process:
lsof -i :5000thenkill -9 PID - Or change the PORT in
.env
Issue: “MODULE_NOT_FOUND”
- Run
npm installto install missing dependencies
Final Thoughts
Building a REST API doesn’t have to be complicated. With Node.js and Express, you can have a production-ready API in minutes. The key is knowing what tools to use and having a solid structure from the start.
This setup scales beautifully—I’ve used this exact architecture for APIs handling millions of requests. The beauty is in its simplicity: add more models, more routes, more controllers, and your API grows naturally.
Share Your Success
Did you build something cool with this tutorial? I want to hear about it! Share your project in the comments or reach out on Twitter.
Follow for more: Quick tutorials, system design insights, and the human side of software engineering.
Resources:
Built something awesome? Drop a comment below—I read every single one. 🚀
Discussion
Loading comments...