🛠️ Setting Up an Express Application
Knowing what Express is won't put a server on localhost — building one will. In this lesson you'll take an empty folder all the way to a live, auto-reloading Express server, with a project layout that won't collapse as your app grows.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Initialize a Node project with npm and understand
package.json - Install Express and write a minimal
app.jsthat starts a server - Add npm scripts and a
nodemondev server for automatic reloads - Read configuration from environment variables with a
.envfile - Lay out a starter project structure that scales beyond a single file
Estimated Time: 30–40 minutes • Difficulty: Beginner
Hands-on: Scaffold a complete, runnable Express project from scratch and verify it in the browser.
In This Lesson
Before You Start
You need Node.js installed (which includes npm, the Node package manager). Use a current LTS release. Confirm both are present:
node --version # e.g. v20.x or v22.x
npm --version # e.g. 10.x
📖 Key Terms
npm: the tool that installs packages and records them in your project.
package.json: the manifest describing your project — its name, scripts, and dependencies.
Dependency: a package your app needs to run (like Express itself).
The whole setup is five short steps. Here's the map before we walk it:
Step 1 — Initialize the Project
Create a folder for the app and turn it into a Node project. The -y flag accepts all defaults so npm doesn't ask questions.
mkdir my-express-app
cd my-express-app
npm init -y
This creates a package.json. Open it and you'll see something like this. Add "type": "module" only if you plan to use modern import syntax — this course uses CommonJS require, so we'll leave it out here.
{
"name": "my-express-app",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
💡 The main field
Change "main" to "app.js" (or whatever your entry file is). It tells other tools which file starts your app. It's a documentation convenience, not a hard requirement.
Step 2 — Install Express
Install Express and save it as a dependency (npm does this automatically now):
npm install express
Two things change. First, package.json gains a dependencies entry:
"dependencies": {
"express": "^4.19.2"
}
Second, a node_modules/ folder and a package-lock.json appear. You never commit node_modules to Git — it's huge and reproducible. Create a .gitignore:
node_modules/
.env
⚠️ Express 4 vs. Express 5
Express 5 is now stable. Its API is nearly identical for everything in this course; the biggest change is that async route handlers that reject are automatically forwarded to your error handler — no more wrapping every handler in try/catch. The ^4.19.2 shown above is a safe default; if you install Express 5, everything here still works.
Step 3 — Write the Server
Create app.js. This is the smallest useful Express server — it creates an app, defines one route, and starts listening.
// app.js
const express = require('express');
// Create the application object
const app = express();
// Read the port from the environment, or default to 3000
const PORT = process.env.PORT || 3000;
// A single route: respond to GET / with some text
app.get('/', (req, res) => {
res.send('Hello from Express!');
});
// A JSON route to prove the server is alive
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
// Start listening for requests
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
Run it:
node app.js
Terminal output:
Server running at http://localhost:3000
Open http://localhost:3000 to see the text, and /api/health to see JSON. Press Ctrl+C in the terminal to stop the server.
app.listen(PORT) keeps the process alive, bound to a port, answering each browser request until you stop it.Step 4 — Scripts & nodemon
Restarting the server by hand after every edit gets old fast. nodemon watches your files and restarts automatically. Install it as a dev dependency (it isn't needed in production):
npm install --save-dev nodemon
Add scripts to package.json so you can type short commands instead of remembering full ones:
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
}
Now use:
npm run dev # development: auto-restarts on save
npm start # production-style: plain node
✅ Why scripts matter
Scripts are the standard front door to a project. A new teammate can run npm run dev without knowing your entry file's name. Every serious Node project defines at least start and dev.
Step 5 — Environment Config
Hard-coding values like ports, database URLs, and secret keys is a mistake — they differ between your laptop and the production server, and secrets must never live in source control. The standard fix is environment variables, loaded from a .env file in development.
Recent Node versions (20.6+) can load a .env file natively with a flag, but the widely-used dotenv package works everywhere:
npm install dotenv
Create a .env file (already in .gitignore from Step 2):
PORT=4000
APP_NAME=My Express App
Load it at the very top of app.js, before you read any variable:
// app.js
require('dotenv').config(); // must run first
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const APP_NAME = process.env.APP_NAME || 'Express App';
app.get('/', (req, res) => {
res.send(`Hello from ${APP_NAME}!`);
});
app.listen(PORT, () => {
console.log(`${APP_NAME} running at http://localhost:${PORT}`);
});
⚠️ Never commit secrets
Commit a .env.example with the keys but not the real values, so teammates know what to fill in. The real .env — with actual secrets — stays out of Git forever.
Project Structure
A one-file app.js is fine to start, but real apps split responsibilities into folders. Here's a sensible starter layout you'll grow into over this module:
my-express-app/
├── node_modules/ # installed packages (git-ignored)
├── src/
│ ├── routes/ # route definitions (users.js, products.js)
│ ├── controllers/ # request-handling logic
│ ├── middleware/ # custom middleware
│ └── app.js # creates and configures the Express app
├── public/ # static files (css, images)
├── .env # local secrets (git-ignored)
├── .env.example # documented, safe-to-commit template
├── .gitignore
├── package.json
└── package-lock.json
💡 Separate "create the app" from "start the app"
A common pro pattern: src/app.js builds and exports the configured app, and a tiny server.js imports it and calls app.listen(). That split lets your tests import the app without ever opening a network port.
// src/app.js
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello!'));
module.exports = app;
// server.js
require('dotenv').config();
const app = require('./src/app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`On http://localhost:${PORT}`));
Hands-on Exercise
🏋️ Scaffold a Runnable Express Project
Objective: Build the complete setup end to end and prove it works.
Instructions:
- Create a folder, run
npm init -y, and installexpress, plusdotenv, plusnodemonas a dev dependency. - Add a
.gitignorethat ignoresnode_modules/and.env. - Add
startanddevscripts topackage.json. - Create a
.envwithPORT=4000. - Write
app.jsthat loads dotenv, listens onprocess.env.PORT, and servesGET /(text) andGET /api/health(JSON with astatusfield). - Run
npm run dev, confirm it reports port 4000, then edit the/response and watch nodemon restart.
💡 Hint
The order in app.js matters: require('dotenv').config() must run before you read process.env.PORT, or the variable will still be undefined and you'll fall back to 3000.
✅ Sample solution
// app.js
require('dotenv').config();
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.send('Setup complete!');
});
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
// package.json (scripts)
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
}
Running npm run dev should log Server running at http://localhost:4000, and editing any watched file triggers an automatic restart.
Quiz
🎯 Check Your Understanding
Question 1: Why should node_modules/ be listed in .gitignore?
Question 2: What is the main benefit of running your server with nodemon during development?
Question 3: Where must require('dotenv').config() appear for process.env.PORT to be defined?
Summary
🎉 Key Takeaways
- npm init creates
package.json; npm install express adds the dependency andnode_modules. - A minimal server is
express()+ a route +app.listen(PORT). - npm scripts (
start,dev) are the standard entry points; nodemon auto-restarts in development. - Environment variables (via
dotenv) keep config and secrets out of your code and out of Git. - A routes / controllers / middleware folder layout — and splitting "build the app" from "start the app" — scales cleanly.
📚 Further Reading
🚀 What's Next?
Your server is running but only answers a couple of URLs. Next up, Basic Routing and Request Handling teaches you to map many methods and paths to handlers, capture route and query parameters, and shape responses properly.
🎉 It's alive!
You have a real, auto-reloading Express server. Now let's teach it to handle many routes.