So you built your first Node.js app?
Nice. You've got the basic server, you're handling GET and POST requests, and you’re hyped.
But let’s be real — writing everything from scratch with the native http
module gets messy real quick.
That’s where Express.js enters the chat.
Wait, what even is Express? 🤔
Express.js is a minimalist and flexible Node.js web application framework
that provides a robust set of features for building web and mobile applications.
Translation? It handles all the annoying parts for you — routing, parsing JSON, handling errors, and more — so you can focus on building.
Step 1: Setup the Project (Express-style) 🛠️
If you’ve got your notes-api
folder already, you can work from there.
Or create a fresh one if you want a clean start:
mkdir express-notes-api
cd express-notes-api
npm init -y
npm install express
Create your main file:
touch index.js
Step 2: Create Your First Express App 💡
Inside index.js
:
const express = require("express");
const app = express();
app.use(express.json()); // Middleware to parse JSON bodies
// Sample data
let notes = [
{ id: 1, text: "Learn Node.js" },
{ id: 2, text: "Move to Express.js" },
];
// GET all notes
app.get("/notes", (req, res) => {
res.json(notes);
});
// POST a new note
app.post("/notes", (req, res) => {
const newNote = {
id: notes.length + 1,
text: req.body.text,
};
notes.push(newNote);
res.status(201).json(newNote);
});
// Server listener
app.listen(3000, () => {
console.log("🚀 Server running at http://localhost:3000");
});
Now run:
node index.js
And boom — you’ve got a fully working REST API in like 15 lines.
Step 3: Refactor into Routes 🧱
Right now, everything lives in index.js
, which is fine for demos but not for real projects.
Let’s split the routes out.
Folder structure:
express-notes-api/
├── routes/
│ └── notes.js
├── index.js
├── package.json
routes/notes.js
const express = require("express");
const router = express.Router();
let notes = [
{ id: 1, text: "Learn Node.js" },
{ id: 2, text: "Move to Express.js" },
];
router.get("/", (req, res) => {
res.json(notes);
});
router.post("/", (req, res) => {
const newNote = {
id: notes.length + 1,
text: req.body.text,
};
notes.push(newNote);
res.status(201).json(newNote);
});
module.exports = router;
Update index.js
:
const express = require("express");
const app = express();
app.use(express.json());
app.use("/notes", require("./routes/notes"));
app.listen(3000, () => {
console.log("🚀 Server running at http://localhost:3000");
});
Congrats — you’ve officially modularized your API 🔥
Step 4: Add Error Handling 🧯
Let’s make your app a bit more robust:
// In routes/notes.js
router.get("/:id", (req, res) => {
const note = notes.find(n => n.id === parseInt(req.params.id));
if (!note) return res.status(404).json({ error: "Note not found" });
res.json(note);
});
Now you can GET /notes/1
or /notes/2
— and gracefully handle not-found cases.
Step 5: Use Middleware (like a boss) ⚙️
Middleware are functions that run before your route handlers. For example:
// index.js
// Logger middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // Don't forget this!
});
Now every request logs to your terminal.
Want to use 3rd-party middleware? Try cors
or morgan
:
npm install cors morgan
const cors = require("cors");
const morgan = require("morgan");
app.use(cors());
app.use(morgan("dev"));
Now it’s starting to feel like a real app 🚀
Step 6: Add File Persistence with fs
(optional) 💾
Let’s store notes in a JSON file:
const fs = require("fs");
const path = "./data.json";
// Save to file
function saveNotes(notes) {
fs.writeFileSync(path, JSON.stringify(notes, null, 2));
}
// Load from file
function loadNotes() {
if (!fs.existsSync(path)) return [];
return JSON.parse(fs.readFileSync(path));
}
let notes = loadNotes();
// In POST route
saveNotes(notes);
Now your notes persist even after restarting the server. Welcome to the big leagues.
Step 7: Add Environment Variables 🌱
Don’t hardcode values like ports in production. Use dotenv
.
npm install dotenv
Create a .env
file:
PORT=5000
In index.js
:
require("dotenv").config();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🚀 Server running at http://localhost:${PORT}`);
});
Much cleaner. Much safer.
Step 8: Final Folder Structure 📁
express-notes-api/
├── routes/
│ └── notes.js
├── data.json
├── .env
├── index.js
├── package.json
You're now running a modular, clean, production-ish API. And it still uses plain ol' JavaScript. No TypeScript, no DB (yet). Just Node + Express, done right.
What’s Next? 📈
You could...
- Add validation with Joi or Zod
- Plug in MongoDB or PostgreSQL
- Use TypeScript for type safety
- Add tests with Jest or Supertest
- Deploy to Render, Railway, or Fly.io
- Secure your API with auth middleware
- Add rate limiting, pagination, etc.
Each of these can be a next blog. If you want me to write any of those parts — you know what to do 🧠
Final Words
Building with Express.js is like going from driving a bicycle to a smooth electric scooter.
Still simple. But way faster, cooler, and easier to scale.
If you made it this far — take a break, sip water, and be proud. You leveled up today.
Drop a star on an open-source project. Build your own. Or just play around with routes and see what breaks.
Keep it clean. Keep it expressive.
See you in Part 3 (maybe with databases or auth 👀).