Getting Started with Express
Build your first web server and understand how HTTP requests flow through your application.
What Even Is a Web Server?#
When you type a URL in your browser and hit enter, something magical happens. Your browser sends a request across the internet to a computer somewhere in the world. That computer runs a program that receives your request, figures out what you want, and sends back a response.
That program is a web server. And you're about to build one.
Think of it like a restaurant. A customer (the browser) walks in and asks for something (the request). The kitchen (your server) prepares what they asked for and brings it out (the response). Express is the framework that makes building this kitchen easy.
Your First Server (It's Simpler Than You Think)#
Let's create a server that responds to requests. First, set up your project:
mkdir my-first-api
cd my-first-api
npm init -y
npm install express
Now create a file called index.js:
import express from 'express';
// Create an Express application
const app = express();
// When someone visits the root URL ("/"), respond with a message
app.get('/', (req, res) => {
res.send('Hello! Your server is working!');
});
// Start listening for requests on port 3000
app.listen(3000, () => {
console.log('Server is running at http://localhost:3000');
});
Add "type": "module" to your package.json, then run:
node index.js
Open your browser to http://localhost:3000. You should see "Hello! Your server is working!"
Congratulations - you just built a web server. Every website, API, and web app you've ever used has something like this running behind the scenes.
Understanding What Just Happened#
Let's break down that code:
const app = express();
This creates your application - an object that can receive requests and send responses.
app.get('/', (req, res) => {
res.send('Hello! Your server is working!');
});
This says: "When someone makes a GET request to /, run this function." The function receives two objects:
req(request) - information about what the user asked forres(response) - tools to send something back
app.listen(3000, () => { ... });
This tells your server to start listening for requests on port 3000. Think of ports like apartment numbers - they help direct traffic to the right application on a computer.
HTTP Methods: The Verbs of the Web#
When a browser talks to a server, it uses different "verbs" depending on what it wants to do. These are called HTTP methods.
Imagine a to-do list app. You'd need to:
- See your tasks (read data)
- Add new tasks (create data)
- Check off tasks (update data)
- Delete tasks (remove data)
Each action uses a different HTTP method:
// GET - "Give me something"
// Used for reading/fetching data
app.get('/todos', (req, res) => {
res.send('Here are your todos');
});
// POST - "Here's something new"
// Used for creating data
app.post('/todos', (req, res) => {
res.send('Todo created');
});
// PUT - "Replace this thing"
// Used for completely replacing data
app.put('/todos/1', (req, res) => {
res.send('Todo 1 replaced');
});
// PATCH - "Update part of this thing"
// Used for partial updates
app.patch('/todos/1', (req, res) => {
res.send('Todo 1 updated');
});
// DELETE - "Remove this thing"
// Used for deleting data
app.delete('/todos/1', (req, res) => {
res.send('Todo 1 deleted');
});
How Do You Test These?
GET requests are easy - just visit the URL in your browser. For POST, PUT, PATCH, DELETE, use a tool like Postman, Insomnia, or the curl command in your terminal.
Dynamic Routes: Handling Different Data#
Real apps don't have one user or one todo. They have thousands. You need routes that can handle any ID:
// The :id part is a "route parameter"
// It matches anything: /users/1, /users/42, /users/abc
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
res.send(`You requested user ${userId}`);
});
Visit http://localhost:3000/users/42 and you'll see "You requested user 42".
You can have multiple parameters:
// /users/5/posts/10
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.send(`User ${userId}, Post ${postId}`);
});
Query Parameters: Optional Extras#
URLs can have extra information after a ?. These are query parameters, and they're great for optional stuff like filtering and pagination:
/users?page=2&limit=10&sort=name
Access them through req.query:
app.get('/users', (req, res) => {
const page = req.query.page || 1;
const limit = req.query.limit || 10;
const sort = req.query.sort;
res.send(`Page ${page}, Limit ${limit}, Sort by ${sort}`);
});
| Feature | Type | Example URL | Access With |
|---|---|---|---|
| Route params | /users/42 | req.params.id | |
| Query params | /users?page=2 | req.query.page |
When to use which? Route params for required identifiers (/users/42). Query params for optional filters (/users?status=active).
Receiving Data: The Request Body#
When users create or update data, they send information in the request body. This is common with POST and PUT requests.
But there's a catch: Express doesn't understand request bodies by default. You need to tell it how:
// This line is ESSENTIAL - without it, req.body is undefined
app.use(express.json());
app.post('/users', (req, res) => {
const { name, email } = req.body;
res.send(`Creating user: ${name} (${email})`);
});
Now you can send JSON data to your server:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Sarah", "email": "sarah@example.com"}'
Sending Responses: Different Ways to Reply#
You've seen res.send(), but there are better options:
app.get('/examples', (req, res) => {
// Send JSON (most common for APIs)
res.json({ message: 'Hello', count: 42 });
});
app.get('/created', (req, res) => {
// Send JSON with a specific status code
res.status(201).json({ id: 1, name: 'New Item' });
});
app.get('/error', (req, res) => {
// Send an error response
res.status(404).json({ error: 'Not found' });
});
app.get('/redirect', (req, res) => {
// Redirect to another URL
res.redirect('/somewhere-else');
});
Status Codes: What They Mean#
Status codes tell the client what happened:
| Range | Meaning | Examples |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirect | 301 Moved, 302 Found |
| 4xx | Client Error | 400 Bad Request, 401 Unauthorized, 404 Not Found |
| 5xx | Server Error | 500 Internal Error, 503 Unavailable |
The Most Common Ones
200 - Everything worked. 201 - Created something new. 400 - You sent bad data. 401 - You're not logged in. 404 - That thing doesn't exist. 500 - We messed up (server bug).
Organizing Routes: When Things Get Bigger#
Having all routes in one file gets messy fast. Express has a Router to help:
// routes/users.js
import { Router } from 'express';
const router = Router();
router.get('/', (req, res) => {
res.json({ users: ['Alice', 'Bob', 'Charlie'] });
});
router.get('/:id', (req, res) => {
res.json({ user: `User ${req.params.id}` });
});
router.post('/', (req, res) => {
res.status(201).json({ message: 'User created' });
});
export default router;
// index.js
import express from 'express';
import userRoutes from './routes/users.js';
const app = express();
app.use(express.json());
// Mount the router at /api/users
app.use('/api/users', userRoutes);
// Now you have:
// GET /api/users
// GET /api/users/:id
// POST /api/users
app.listen(3000);
This keeps your code organized and each file focused on one thing.
The Async Trap (Important!)#
Most real code is asynchronous - reading from databases, calling other APIs. Here's a trap many developers fall into:
// DANGER: This will crash your server if the database throws an error
app.get('/users/:id', async (req, res) => {
const user = await database.findUser(req.params.id);
res.json(user);
});
If database.findUser() throws an error, Express doesn't catch it. Your server crashes.
Always use try/catch with async routes:
app.get('/users/:id', async (req, res) => {
try {
const user = await database.findUser(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
console.error('Database error:', error);
res.status(500).json({ error: 'Something went wrong' });
}
});
This is verbose, so many developers create a wrapper:
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Cleaner
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await database.findUser(req.params.id);
res.json(user);
}));
We'll cover error handling in detail in the Middleware section.
Environment Variables: Don't Hardcode Secrets#
Never put passwords, API keys, or configuration in your code:
// BAD - anyone who sees your code sees your secrets
const DB_PASSWORD = 'super_secret_password';
// GOOD - read from environment variables
const DB_PASSWORD = process.env.DB_PASSWORD;
Use a .env file for local development:
# .env (add this file to .gitignore!)
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key
Load it with the dotenv package:
import 'dotenv/config';
const PORT = process.env.PORT || 3000;
app.listen(PORT);
Putting It All Together#
Here's a complete, well-structured starter:
import 'dotenv/config';
import express from 'express';
const app = express();
const PORT = process.env.PORT || 3000;
// Parse JSON request bodies
app.use(express.json());
// In-memory "database" for demo
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
// GET all users
app.get('/api/users', (req, res) => {
res.json(users);
});
// GET one user
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// CREATE user
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email required' });
}
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Key Takeaways#
You now understand the fundamentals of Express:
- Routes define what URLs your server responds to
- HTTP methods (GET, POST, PUT, DELETE) indicate the action
- Route params (
/:id) capture dynamic parts of URLs - Query params (
?page=2) handle optional data - req.body contains data sent with POST/PUT requests (needs
express.json()) - res.json() sends JSON responses with appropriate status codes
- Router helps organize routes into separate files
- Always handle errors in async routes with try/catch
What's Next
You can build basic APIs now, but real applications need more: authentication, validation, logging, error handling. That's where middleware comes in - the secret sauce that makes Express powerful.
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.