Node.js Essentials: Building a Web App with Express
Express.js is the de facto standard web framework for Node.js. Its minimalist design, powerful routing, and flexible middleware system make it perfect for building everything from REST APIs to full-stack web applications. In this tutorial, you’ll build a complete blog application from scratch.
What You’ll Build
By the end of this tutorial, you’ll have created a functional blog application with:
- A home page displaying all blog posts
- Individual post pages with dynamic routing
- A simple form to create new posts
- Clean URLs and proper error handling
This builds on the HTTP server knowledge from our previous tutorial on Building an HTTP Server.
Setting Up Your Project
First, create a new directory and initialize your project:
mkdir my-blog && cd my-blog
npm init -y
Now install Express:
npm install express
Create a file called app.js:
const express = require('express');
const app = express();
const port = 3000;
app.listen(port, () => {
console.log(`Blog app listening at http://localhost:${port}`);
});
Run it:
node app.js
You’ll see: Blog app listening at http://localhost:3000
Understanding Middleware
Middleware functions are the backbone of Express. They’re functions that have access to the request object (req), response object (res), and the next middleware function (next).
Here’s how a typical request flows through middleware:
// Middleware that logs each request
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // Passes control to the next middleware
});
// Another middleware example
app.use((req, res, next) => {
req.timestamp = new Date(); // Add custom data to request
next();
});
Pro Tip: Middleware order matters! Define your middleware before your routes to ensure it runs on every request.
Working with Route Parameters
Express makes dynamic routing simple with route parameters. Here’s how to create a blog post page:
// Sample blog data
const posts = {
'1': {
title: 'Getting Started with Node.js',
content: 'Node.js is a JavaScript runtime built on Chrome\'s V8 engine...'
},
'2': {
title: 'Understanding Express Middleware',
content: 'Middleware functions are the building blocks of Express apps...'
}
};
// Route parameter - :id captures the dynamic part
app.get('/posts/:id', (req, res) => {
const post = posts[req.params.id];
if (!post) {
return res.status(404).send('Post not found');
}
res.send(`
<h1>${post.title}</h1>
<p>${post.content}</p>
<a href="/">Back to home</a>
`);
});
Visit http://localhost:3000/posts/1 to see your first post.
Processing Form Data
To handle form submissions, you’ll need middleware to parse the request body:
npm install body-parser
Then configure it in your app:
const bodyParser = require('body-parser');
// Parse JSON bodies
app.use(bodyParser.json());
// Parse URL-encoded bodies (forms)
app.use(bodyParser.urlencoded({ extended: true }));
// Handle form submission
app.post('/posts', (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).send('Title and content are required');
}
const id = Date.now().toString();
posts[id] = { title, content };
res.redirect(`/posts/${id}`);
});
Create an HTML form:
app.get('/new-post', (req, res) => {
res.send(`
<form action="/posts" method="POST">
<input type="text" name="title" placeholder="Post title" required>
<textarea name="content" placeholder="Post content" required></textarea>
<button type="submit">Publish</button>
</form>
`);
});
Building the Complete Blog App
Here’s the full application combining everything we’ve learned:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
// Middleware
app.use(bodyParser.urlencoded({ extended: true }));
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
next();
});
// In-memory storage (use a database in production)
const posts = {};
// Home page - list all posts
app.get('/', (req, res) => {
const postList = Object.entries(posts).map(([id, post]) => `
<li><a href="/posts/${id}">${post.title}</a></li>
`).join('');
res.send(`
<h1>My Blog</h1>
<ul>${postList || '<li>No posts yet</li>'}</li></ul>
<a href="/new-post">Create new post</a>
`);
});
// New post form
app.get('/new-post', (req, res) => {
res.send(`
<form action="/posts" method="POST">
<div>
<label>Title:</label>
<input type="text" name="title" required>
</div>
<div>
<label>Content:</label>
<textarea name="content" required></textarea>
</div>
<button type="submit">Publish</button>
</form>
`);
});
// Create new post
app.post('/posts', (req, res) => {
const { title, content } = req.body;
const id = Date.now().toString();
posts[id] = { title, content };
res.redirect(`/posts/${id}`);
});
// View single post
app.get('/posts/:id', (req, res) => {
const post = posts[req.params.id];
if (!post) {
return res.status(404).send('Post not found');
}
res.send(`
<h1>${post.title}</h1>
<p>${post.content}</p>
<a href="/">← Back to home</a>
`);
});
// 404 handler
app.use((req, res) => {
res.status(404).send('Page not found');
});
app.listen(port, () => {
console.log(`Blog app running at http://localhost:${port}`);
});
Serving Static Files
For a real application, you’ll want to serve CSS, images, and client-side JavaScript. Use the built-in static middleware:
// Serve files from the 'public' directory
app.use(express.static('public'));
// With a mount path
app.use('/assets', express.static('public'));
Now you can put CSS files in a public folder and link them like:
<link rel="stylesheet" href="/assets/style.css">
Frequently Asked Questions
What’s the difference between app.use() and app.get()?
app.use() is for middleware that runs on every request, regardless of the HTTP method. app.get() specifically handles GET requests. There are also app.post(), app.put(), app.delete() for other HTTP methods.
How do I handle errors in Express?
Express has a special middleware signature for errors. Define it at the end of your middleware chain:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something went wrong!');
});
Can I use async functions in Express routes?
Yes! Express supports async route handlers. Just wrap your async code and pass any errors to next():
app.get('/async-route', async (req, res, next) => {
try {
const data = await someAsyncOperation();
res.json(data);
} catch (error) {
next(error);
}
});
Summary
You’ve learned how to:
- Set up an Express application
- Create and configure middleware
- Build dynamic routes with parameters
- Process form data with body-parser
- Serve static files
- Handle errors and 404s
This blog app uses in-memory storage—perfect for learning but not for production. In a real application, you’d connect to a database like MongoDB, PostgreSQL, or even a simple JSON file.
Next Steps
Now that you’ve mastered Express basics, continue with the Node.js Essentials series:
- Streams in Node.js — Learn about data streams
- Worker Threads in Node.js — Parallel processing