Skip to main content

🛠️ 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.js that starts a server
  • Add npm scripts and a nodemon dev server for automatic reloads
  • Read configuration from environment variables with a .env file
  • 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:

flowchart LR A[npm init] --> B[npm install express] B --> C[Write app.js] C --> D[Add npm scripts + nodemon] D --> E[.env config] E --> F[Running server]

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.

What app.listen does The app.js file starts a process that binds to a port and waits for browser requests, replying with responses. app.js node app.js Listening port 3000 Browser localhost:3000 request response
Figure 1 — 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:

  1. Create a folder, run npm init -y, and install express, plus dotenv, plus nodemon as a dev dependency.
  2. Add a .gitignore that ignores node_modules/ and .env.
  3. Add start and dev scripts to package.json.
  4. Create a .env with PORT=4000.
  5. Write app.js that loads dotenv, listens on process.env.PORT, and serves GET / (text) and GET /api/health (JSON with a status field).
  6. 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 and node_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.