Kick off a small but complete Node.js project and you can have a working API in under an hour. This crash course walks through a minimal Express server, CRUD endpoints, basic validation and testing tips so you can ship or iterate quickly.
Why build a tiny API first
A small API provides a focused playground for routing, middleware, error handling and deployment wiring. You’ll learn patterns (and pitfalls) that scale into larger services without getting bogged down in enterprise boilerplate.
Prerequisites
- Node.js v14+ installed
- A terminal and basic knowledge of npm/yarn
- Optional: curl or HTTP client (Postman, HTTPie)
Quick project setup
Open a terminal and run:
mkdir node-crash-api && cd node-crash-api
npm init -y
npm install express
npm install --save-dev nodemon
Update package.json scripts to add a dev script (optional):
{
"name": "node-crash-api",
"version": "1.0.0",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
}
Create the server (index.js)
This example uses an in-memory store so you can focus on routing and behavior. Replace with a database later.
const express = require('express')
const app = express()
const port = process.env.PORT || 3000
// simple in-memory data store
const users = \[\]
let idCounter = 1
app.use(express.json())
// health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' })
})
// list users
app.get('/api/users', (req, res) => {
res.json(users)
})
// get user by id
app.get('/api/users/:id', (req, res) => {
const id = Number(req.params.id)
const user = users.find(u => u.id === 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: 'Missing name or email' })
const user = { id: idCounter++, name, email }
users.push(user)
res.status(201).json(user)
})
// update user
app.put('/api/users/:id', (req, res) => {
const id = Number(req.params.id)
const idx = users.findIndex(u => u.id === id)
if (idx === -1) return res.status(404).json({ error: 'User not found' })
const { name, email } = req.body
if (!name && !email) return res.status(400).json({ error: 'Provide name or email to update' })
const updated = Object.assign({}, users[idx], { name: name || users[idx].name, email: email || users[idx].email })
users[idx] = updated
res.json(updated)
})
// delete user
app.delete('/api/users/:id', (req, res) => {
const id = Number(req.params.id)
const idx = users.findIndex(u => u.id === id)
if (idx === -1) return res.status(404).json({ error: 'User not found' })
users.splice(idx, 1)
res.status(204).send()
})
// global error handler (basic)
app.use((err, req, res, next) => {
console.error(err)
res.status(500).json({ error: 'Internal server error' })
})
app.listen(port, () => console.log(`Server running on http://localhost:${port}`))
Notes:
- The users array is ephemeral and will reset when the server restarts.
- Using a numeric idCounter keeps things simple.
Test your API quickly
Start the server:
npm run dev
Create a user with curl:
curl -s -X POST -H "Content-Type: application/json" -d '{"name":"Alice","email":"alice@example.com"}' http://localhost:3000/api/users | jq
List users:
curl -s http://localhost:3000/api/users | jq
Get a user:
curl -s http://localhost:3000/api/users/1 | jq
Update:
curl -s -X PUT -H "Content-Type: application/json" -d '{"email":"new@example.com"}' http://localhost:3000/api/users/1 | jq
Delete:
curl -s -X DELETE http://localhost:3000/api/users/1 -I
(If you don’t have jq, you can omit the pipe to jq and the output will be raw JSON.)
Add validation and small improvements
For production-ready input validation, use a library like express-validator or Zod. A small manual guard is fine for a demo:
// lightweight validation example
function validateUserPayload(req, res, next) {
const { name, email } = req.body
if (typeof name !== 'string' || typeof email !== 'string') {
return res.status(400).json({ error: 'Invalid payload' })
}
next()
}
app.post('/api/users', validateUserPayload, (req, res) => { /* ... */ })
Also consider:
- Rate limiting (express-rate-limit)
- Helmet for secure headers (helmet)
- CORS configuration (cors)
Environment config and scripts
Use environment variables for configurable values. For local development, dotenv is handy.
npm install dotenv
Then at top of index.js:
require('dotenv').config()
const port = process.env.PORT || 3000
Deploying quickly
- Heroku/GitHub Codespaces: push your repo and set the start script.
- Vercel / Render: both support Node.js services; point to port and start script.
- Docker: containerize if you need the added portability.
A minimal Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . ./
CMD ["node", "index.js"]
Next steps (scale from here)
- Replace the in-memory store with a database (Postgres, MongoDB)
- Add authentication (JWT, sessions)
- Add logging (winston or pino)
- Add tests (supertest + jest)
- Add CI pipeline for linting and tests
Troubleshooting & tips
- 502/503 on cloud platforms usually means port mismatch — read the host’s PORT var.
- Use nodemon in dev to auto-restart on file changes.
- Keep responses consistent: always return JSON and proper status codes.
Conclusion
In under an hour you can scaffold, implement and test a small REST API with Node.js and Express. This minimal footprint teaches the core concepts you need before adding persistence, auth, and production concerns. Fork the code, substitute a real DB, and build from there.
