Express remains one of the most popular backend frameworks in the JavaScript ecosystem because it’s minimal, flexible, and easy to reason about. When you pair it with ES6 syntax and MongoDB, you get a lightweight but powerful stack that’s perfect for APIs, side projects, MVPs, and even production services.
In this post, we’ll build a simple Blog REST API using:
- Node.js
- Express
- ES6 modules
- MongoDB
- Mongoose
We’ll focus on clarity, clean structure, and real-world patterns — not just getting something to work, but building something you’d actually want to maintain.
What We’re Building
By the end of this tutorial, we’ll have a REST API that supports:
- Creating blog posts
- Retrieving all posts
- Retrieving a single post
- Updating a post
- Deleting a post
In short: full CRUD functionality for a blog.
This API will serve as a solid foundation you can later extend with:
- Authentication
- Pagination
- Comments
- Tags
- Admin dashboards
Why Express + ES6 + MongoDB?
Before jumping into code, let’s talk about why this stack works so well.
Express
Express gives you:
- Full control over request handling
- Minimal abstractions
- A massive ecosystem of middleware
It doesn’t force architectural decisions — which is both a strength and a responsibility.
ES6 Modules
Using import and export:
- Improves readability
- Matches frontend JavaScript
- Makes refactoring easier
- Encourages better file organization
MongoDB
MongoDB is a great fit for blog content because:
- Blog posts are document-shaped
- Schemas can evolve
- Nested fields are easy
- JSON-like structure feels natural in JavaScript
Project Setup
Initialize the project
mkdir blog-api
cd blog-api
npm init -y
Install dependencies:
npm install express mongoose
npm install --save-dev nodemon
Enable ES6 Modules
In package.json, add:
{
"type": "module"
}
This tells Node.js to treat .js files as ES modules, allowing us to use import and export.
Project Structure
A clean structure makes even small projects easier to extend.
src/
server.js
config/
database.js
models/
Post.js
routes/
postRoutes.js
controllers/
postController.js
Each layer has a single responsibility:
- Models → data schema
- Controllers → business logic
- Routes → HTTP endpoints
- Config → infrastructure setup
Connecting to MongoDB
src/config/database.js
import mongoose from "mongoose";
export async function connectDatabase() {
try {
await mongoose.connect("mongodb://localhost:27017/blog-api");
console.log("MongoDB connected");
} catch (error) {
console.error("Database connection failed", error);
process.exit(1);
}
}
In real projects, this connection string should come from environment variables.
Creating the Blog Post Model
src/models/Post.js
import mongoose from "mongoose";
const postSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true
},
content: {
type: String,
required: true
},
author: {
type: String,
default: "Anonymous"
}
},
{
timestamps: true
}
);
export default mongoose.model("Post", postSchema);
Why Mongoose?
Mongoose gives us:
- Schema enforcement
- Validation
- Middleware hooks
- Clean querying APIs
This keeps MongoDB flexible without becoming chaotic.
Creating the Controller
The controller contains the actual logic for handling requests.
src/controllers/postController.js
import Post from "../models/Post.js";
export async function createPost(req, res) {
try {
const post = await Post.create(req.body);
res.status(201).json(post);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
export async function getAllPosts(req, res) {
const posts = await Post.find().sort({ createdAt: -1 });
res.json(posts);
}
export async function getPostById(req, res) {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ message: "Post not found" });
}
res.json(post);
}
export async function updatePost(req, res) {
const post = await Post.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true }
);
if (!post) {
return res.status(404).json({ message: "Post not found" });
}
res.json(post);
}
export async function deletePost(req, res) {
const post = await Post.findByIdAndDelete(req.params.id);
if (!post) {
return res.status(404).json({ message: "Post not found" });
}
res.status(204).send();
}
Each function does one thing and does it clearly.
Defining Routes
Routes map HTTP verbs and URLs to controller actions.
src/routes/postRoutes.js
import { Router } from "express";
import {
createPost,
getAllPosts,
getPostById,
updatePost,
deletePost
} from "../controllers/postController.js";
const router = Router();
router.post("/", createPost);
router.get("/", getAllPosts);
router.get("/:id", getPostById);
router.put("/:id", updatePost);
router.delete("/:id", deletePost);
export default router;
Setting Up the Express Server
src/server.js
import express from "express";
import { connectDatabase } from "./config/database.js";
import postRoutes from "./routes/postRoutes.js";
const app = express();
// Middleware
app.use(express.json());
// Routes
app.use("/api/posts", postRoutes);
// Start server
const PORT = process.env.PORT || 3000;
connectDatabase().then(() => {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
});
Running the API
Add a dev script to package.json:
{
"scripts": {
"dev": "nodemon src/server.js"
}
}
Run the server:
npm run dev
Testing the API
Create a post
POST /api/posts
{
"title": "My First Blog Post",
"content": "This is the content of my first post",
"author": "James"
}
Get all posts
GET /api/posts
Get a single post
GET /api/posts/{id}
Update a post
PUT /api/posts/{id}
{
"title": "Updated Title",
"content": "Updated content"
}
Delete a post
DELETE /api/posts/{id}
Common Mistakes to Avoid
❌ Putting logic in routes
Controllers should handle logic, routes should only map endpoints.
❌ Skipping validation
Always validate request data before saving it.
❌ Not handling missing records
Always return proper 404 responses.
❌ Hardcoding configuration
Use environment variables for database URLs and secrets.
Why This Architecture Scales
This simple Blog API structure:
- Separates concerns cleanly
- Keeps files small and readable
- Allows easy testing
- Makes adding features straightforward
Adding authentication later doesn’t require a rewrite — just new middleware and routes.
Next Steps
Once this API is working, you can extend it with:
- User authentication (JWT)
- Pagination and filtering
- Comments and replies
- Slug-based URLs
- Rate limiting
- Docker deployment
This Express + ES6 + MongoDB foundation is flexible enough to support all of that.
Final Thoughts
Building a simple Blog API with Express, ES6, and MongoDB is one of the best ways to solidify your backend fundamentals. You learn how routing, controllers, models, and databases fit together — without heavy abstractions getting in the way.
If you understand this project, you’re well on your way to building real-world Node.js APIs that scale beyond tutorials.
If you want follow-up posts, I can cover:
- Adding authentication with JWT
- Pagination and search
- Express middleware patterns
- Refactoring this API into a production-grade service
- Dockerizing the stack
Just let me know what you’d like next.