Express foundations

Create the application

Set up an Express 5 project with explicit scripts, environment requirements, and a minimal start file.

The notes app lives in two files from day one: one that builds the Express application, and one that starts listening. Tests import the first. The process runs the second. Keeping them apart is the single most useful habit in this course.

Create the project

Express 5 needs Node 18 or newer. I use the current LTS. Create the folder and install Express:

mkdir notes && cd notes
npm init -y
npm install express

Open package.json and add the module type and the scripts we’ll use:

{
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "dev": "node --watch src/server.js",
    "test": "node --test"
  }
}

"type": "module" lets us write import instead of require. node --watch restarts the process when a file changes, so we don’t need nodemon.

Build the app in one file

src/app.js exports a function that creates and returns the Express application. It does not listen on any port:

import express from 'express'

export function createApp() {
  const app = express()

  app.get('/', (req, res) => {
    res.send('Notes')
  })

  return app
}

Why a function and not a top-level app? Because later we’ll pass dependencies into it, like a notes repository or a fake one for tests. A function gives us a place to accept them.

Start listening in another file

src/server.js is the only file that touches the network:

import { createApp } from './app.js'

const port = Number(process.env.PORT ?? 3000)
const app = createApp()

app.listen(port, () => {
  console.log(`Listening on http://localhost:${port}`)
})

Run npm run dev and you should see:

Listening on http://localhost:3000

Open the URL in the browser and you get Notes.

Prove the split works

The point of the split is that we can use the app without a port. Create test/app.test.js:

import test from 'node:test'
import assert from 'node:assert/strict'
import { createApp } from '../src/app.js'

test('home page responds', async () => {
  const app = createApp()
  const server = app.listen(0)
  const { port } = server.address()
  const res = await fetch(`http://localhost:${port}/`)
  assert.equal(res.status, 200)
  assert.equal(await res.text(), 'Notes')
  server.close()
})

Port 0 asks the operating system for any free port, so tests never fight over 3000. Run npm test and you get one passing test. Later we’ll replace this with supertest, which hides the port dance, but the idea stays the same.

The failure you will see first

Start the server twice and the second process dies with:

Error: listen EADDRINUSE: address already in use :::3000

That’s Node telling you the port is taken. Stop the other process, or run PORT=3001 npm run dev. If you had mixed app and listen() in one file, your test file would hit this error every time the dev server was running. Now it never does.

Lesson completed