Skip to content

Req, res & JSON ​

Most FE ↔ Express traffic is JSON in, JSON out. Learn the helpers and status codes; ignore the rest until you need files or streams.

Reading the request ​

ts
app.post('/api/notes', (req, res) => {
  const { title, body } = req.body // needs express.json()
  const userAgent = req.get('user-agent')
  const auth = req.headers.authorization
  const { id } = req.params
  const page = req.query.page
  // …
})
PieceWhere
JSON bodyreq.body after express.json()
URL paramsreq.params
Queryreq.query
Headersreq.headers or req.get('Authorization')

Bodies are not auto-validated. Garbage in stays garbage until you check (we'll use Zod later).

Writing the response ​

ts
res.status(201).json({ id: 1, title: 'hi' })
res.status(204).end()
res.status(400).json({ error: 'title required' })
res.status(401).json({ error: 'unauthorized' })
res.status(404).json({ error: 'not found' })
res.status(500).json({ error: 'server error' })

Comments for the code above:

  • json() sets Content-Type and stringifies
  • end() for empty bodies (204 No Content)
  • Chain status then json - order is readable that way

Status codes FE people already half-know ​

CodeMeaning in APIs
200OK - here's data
201Created - usually POST success
204Success, no body
400Your fault - validation
401Not authenticated (who are you?)
403Authenticated but not allowed (AuthZ)
404Missing resource
409Conflict (duplicate email…)
422Sometimes used for validation (style choice)
500Our fault

Your SPA should branch on these the same way it already does with if (!res.ok).

Headers you'll care about ​

ts
res.set('X-Request-Id', requestId)
res.json({ ok: true })

CORS headers are usually set by the cors package - don't hand-roll Access-Control-* unless you enjoy pain.

Don't mix HTML and JSON by accident ​

ts
// SPA talks JSON
res.json({ error: 'nope' })

// not:
res.send('<h1>nope</h1>') // surprise HTML for fetch().json()

If you're building a JSON API for a Vite app, stay JSON - even for errors.

Async handlers (gotcha) ​

ts
app.get('/api/notes', async (req, res, next) => {
  try {
    const notes = await db.listNotes()
    res.json({ notes })
  } catch (err) {
    next(err) // forward to error middleware
  }
})

Unhandled promise rejections in Express 4 don't always hit error middleware automatically. Either wrap, or use a tiny helper / Express 5. We'll standardize this in Error handling.

Next: centralizing those next(err) paths so every route doesn't invent its own 500 shape.

Best,

-- ł.

Notes-style Express tutorial for front-end folks