NodeJSYour First Express App

Your First Express App

Time to build something real: a small JSON API for a list of tasks, with routes to list, fetch, create, and delete. Along the way you'll meet the response helpers, body parsing, and the request lifecycle — the everyday tools of Express development. This is the raw-http work from the HTTP section, now expressed cleanly.

The skeleton

server.js

JS
import express from 'express'

const app = express()

// Parse JSON request bodies into req.body (must come before routes):
app.use(express.json())

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

app.listen(3000, () => console.log('http://localhost:3000'))
Register `express.json()` BEFORE routes that read `req.body`
`express.json()` is the body-parsing middleware that reads the request stream and populates `req.body`. Because [middleware runs top-to-bottom](/nodejs/express-intro), it must be registered *before* any route handler that uses `req.body` — otherwise `req.body` is `undefined` in those handlers. This ordering trap catches nearly every Express beginner.
In-memory data (for now)

JS
let tasks = [
  { id: 1, title: 'Learn Express', done: false },
  { id: 2, title: 'Build an API', done: false },
]
let nextId = 3
A real app would use a database
We use an in-memory array to focus on Express itself — restart the server and the data resets. Everything here works identically with a real database; only the data-access calls change. Database integration comes later in the tutorial.
List and fetch (GET)

JS
// GET /tasks → all tasks
app.get('/tasks', (req, res) => {
  res.json(tasks)
})

// GET /tasks/:id → one task (or 404)
app.get('/tasks/:id', (req, res) => {
  const task = tasks.find((t) => t.id === Number(req.params.id))
  if (!task) {
    return res.status(404).json({ error: 'Task not found' })
  }
  res.json(task)
})
`req.params.id` is a string — convert it
Route parameters arrive as **strings** (`req.params.id` is `'1'`, not `1`), so comparing with `===` against a numeric id fails unless you `Number(...)` it. This is the same string-coercion rule as [query parameters](/nodejs/query-parameters). And `return res.status(404)...` — the `return` prevents falling through to send a second response.
Create (POST)

JS
app.post('/tasks', (req, res) => {
  const { title } = req.body            // populated by express.json()

  if (!title || typeof title !== 'string') {
    return res.status(422).json({ error: 'title is required' })
  }

  const task = { id: nextId++, title, done: false }
  tasks.push(task)

  res.status(201).json(task)            // 201 Created + the new resource
})
Always validate `req.body` — clients send anything
`req.body` is untrusted user input. Check that required fields exist and have the right type *before* using them; respond `422` (unprocessable) on bad input. Skipping validation leads to corrupt data and crashes (e.g. calling `.trim()` on `undefined`). The same [validate-everything rule](/nodejs/handling-forms) from raw form handling applies.
Delete (DELETE)

JS
app.delete('/tasks/:id', (req, res) => {
  const id = Number(req.params.id)
  const before = tasks.length
  tasks = tasks.filter((t) => t.id !== id)

  if (tasks.length === before) {
    return res.status(404).json({ error: 'Task not found' })
  }
  res.status(204).end()                 // 204 No Content — nothing to return
})
`204 No Content` for a successful delete — and no body
A successful delete has nothing meaningful to return, so `204` with `res.status(204).end()` is idiomatic — note `.end()`, not `.json()`, because a [204 response must have no body](/nodejs/http-status-codes). Returning the deleted object with `200` is also acceptable; just be consistent across your API.
Response helper cheat sheet

Helper

Does

res.json(obj)

Send JSON (sets Content-Type, stringifies)

res.send(body)

Send string/Buffer/object (infers type)

res.status(code)

Set status (chainable)

res.sendStatus(code)

Set status + send its text

res.redirect(url)

Send a 302 (or given code) redirect

res.set(name, value)

Set a response header

res.end()

End with no body (e.g. 204)

Test it

Bash
curl localhost:3000/tasks
curl localhost:3000/tasks/1
curl -X POST localhost:3000/tasks -H 'Content-Type: application/json' -d '{"title":"Ship it"}'
curl -X DELETE localhost:3000/tasks/2 -i   # -i shows the 204 status line
A note on async handlers and errors
When a handler does async work (database, fetch), make it `async` and `await`. In Express 5, a rejected promise is automatically routed to your error handler; in Express 4 you had to wrap handlers or call `next(err)` manually. We cover robust error handling in the [error-handling middleware](/nodejs/error-middleware) section.
Next
Go deeper on matching requests to handlers: [Express Routing](/nodejs/express-routing).