Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

The request body and a JSON API

A JSON API is a server that receives and returns data as JSON instead of HTML pages. Frontends in React or Vue and mobile apps talk to it. To accept a new post, the server must read the request body — the data the client sent after the headers.

Reading the body

The req object is a stream: the body arrives in chunks. They are collected with a for await loop and joined. Create the file src/body.js:

// src/body.js
export async function readJson(req) {
  const chunks = [];
  for await (const chunk of req) {
    chunks.push(chunk);
  }
  const text = Buffer.concat(chunks).toString('utf8');
  return text === '' ? {} : JSON.parse(text);
}

Buffer.concat() merges the bytes into one piece, toString('utf8') turns them into text. An empty body gives an empty object.

The POST /posts route

Add a branch for creating a post to src/server.js. The addPost function from src/posts.js and sendJson from the routing lesson already exist:

// src/server.js
import { readJson } from './body.js';
import { addPost } from './posts.js';
 
// inside the request handler:
if (req.method === 'POST' && pathname === '/posts') {
  const data = await readJson(req);
  const post = await addPost(data.title, data.body);
  return sendJson(res, 201, post);
}

The 201 Created code tells the client that the record was created. The response contains the post itself — with its assigned number and date.

Trying it out

A browser sends POST only from a form, so an API is easier to test with the curl program in the terminal:

curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "New post", "body": "Post text"}'

The response is an object like {"id":3,"title":"New post",...}, and http://localhost:3000/posts shows the post in the list.

Validating the data

A client can send anything. Before saving, the data is checked, and on error the server answers 400:

// src/server.js
if (req.method === 'POST' && pathname === '/posts') {
  let data;
  try {
    data = await readJson(req);
  } catch {
    return sendJson(res, 400, { error: 'The body is not JSON' });
  }
 
  if (typeof data.title !== 'string' || data.title.trim() === '') {
    return sendJson(res, 400, { error: 'A title is required' });
  }
 
  const post = await addPost(data.title.trim(), String(data.body ?? ''));
  return sendJson(res, 201, post);
}

JSON.parse() throws on malformed text — we catch it and tell the client. A curl request without title gets {"error":"A title is required"}.

Comments

Comments on a post are accepted the same way, at /posts/1/comments. The post number is extracted with the regular expression /^\/posts\/(\d+)\/comments$/, the server checks that the post exists and saves the comment into its comments array. Try writing this branch yourself following the pattern.

Server errors

If an unexpected error happens inside the handler, the server must answer 500 instead of leaving the request hanging. Wrap the handler body in try...catch:

// src/server.js
const server = http.createServer(async (req, res) => {
  try {
    await handle(req, res);
  } catch (error) {
    console.error(error);
    sendJson(res, 500, { error: 'Server error' });
  }
});

Here handle is the function with all the routes from this lesson and the previous one.

Common mistake

Without the Content-Type: application/json header in the response, many clients receive the data as plain text and do not parse it. Send JSON only through one function — sendJson() — and the header is never forgotten in any route.

Thanks for reading!

Donate to the site — we work hard for you.