Get Started¶
You installed Express. Now let's build something that looks like a real project instead of a script kiddie playground
Every production Express app follows the same structure. A single server.js file works for prototypes but falls apart at scale. Routes in one file , middleware in another , config separated , error handling centralized. Structure your project like you expect someone to maintain it - even if that someone is future you at 3AM debugging a prod issue
project setup¶
Your basic Express project structure:
flowchart TD
root["my-express-app/"] --> nm["node_modules/<br/>don't touch , don't commit"]
root --> src["src/"]
src --> routes["routes/<br/>route handlers"]
routes --> ri["index.js"]
routes --> ru["users.js"]
routes --> ra["auth.js"]
src --> mw["middleware/<br/>custom middleware"]
mw --> mwa["auth.js"]
mw --> mwe["errorHandler.js"]
mw --> mwv["validate.js"]
src --> models["models/<br/>data models / DB schemas"]
src --> config["config/<br/>configuration"]
config --> ci["index.js"]
src --> utils["utils/<br/>helpers"]
src --> app["app.js<br/>Express app setup"]
root --> public["public/<br/>static files"]
root --> views["views/<br/>templates"]
root --> tests["tests/<br/>test files"]
root --> env[".env"]
root --> envex[".env.example"]
root --> gitignore[".gitignore"]
root --> pj["package.json"]
root --> server["server.js<br/>entry point"] app.js - the Express app setup¶
Separate your Express app configuration from the server startup
// src/app.js
const express = require('express')
const helmet = require('helmet')
const cors = require('cors')
const morgan = require('morgan')
const indexRoutes = require('./routes/index')
const userRoutes = require('./routes/users')
const app = express()
// global middleware - order matters here
app.use(helmet()) // security headers first
app.use(cors()) // CORS before routes
app.use(morgan('combined')) // logging
app.use(express.json()) // body parsing
app.use(express.urlencoded({ extended: true }))
// routes
app.use('/' , indexRoutes)
app.use('/users' , userRoutes)
// error handler - must be last
app.use((err , req , res , next) => {
console.error(err.stack)
res.status(500).json({ error: 'Something went wrong' })
})
module.exports = app
// server.js - entry point
const app = require('./src/app')
const PORT = process.env.PORT || 3000
app.listen(PORT , () => {
console.log(`Server running on port ${PORT}`)
})
Why separate app from server? Because your tests need to import app without triggering listen(). Supertest will thank you later
Nodemon for dev¶
Restart the server manually every time you change a file is for masochists
npm install --save-dev nodemon
Add to package.json scripts:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}
npm run dev
# [nodemon] watching extensions: js,mjs,json
# [nodemon] starting `node server.js`
# Server running on port 3000
Nodemon watches your files and restarts on every change. No more Ctrl+C , up arrow , Enter loops
Express Generator¶
If you want a full boilerplate dumped into your terminal in 3 seconds
npx express-generator my-app --view=ejs --git
cd my-app
npm install
npm start
This generates a complete Express app with:
- Standard directory structure - routes , public , views , bin
- Selected template engine - ejs , pug , hbs , or jade
- Basic error handling - 404 and 500 handlers included
- .gitignore - because you'd forget
- package.json - with start script
The generated app is fine for learning but needs hardening before production. The error handler leaks stack traces. No Helmet. No CORS config. No rate limiting. Treat it as a starting point not a shipping point
.env configuration with dotenv¶
npm install dotenv
// src/config/index.js
require('dotenv').config()
module.exports = {
port: process.env.PORT || 3000,
db: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
name: process.env.DB_NAME || 'myapp'
},
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV || 'development'
}
# .env - NEVER COMMIT THIS
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
JWT_SECRET=your-secret-here-change-in-production
NODE_ENV=development
Add .env to .gitignore immediately:
node_modules/
.env
.env.local
prerequisites¶
express_01_intro.md - basic server and hello world done
next → express_03_routing.md