Node.js Crash Course: Build a Simple API in Under an Hour

Posted November 17, 2025 by Karol Polakowski

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.