CodingNeed.

3 · Connect, test and build · 35 MIN

Project · A small searchable topics API

Connect a pure function to a thin, validated HTTP handler.

The project keeps calculation separate from transport. selectTopics only sees an array and validated values. The handler checks the route, validates text, calls the function and serializes its result. A try/catch prevents unexpected parsing failures from leaving this small request unanswered. The API is read-only and uses a fixed in-memory dataset, so restarting loses no user data. Real writes, accounts, rate limiting, deployment and durable storage are introduced in the next backend labs.

Run the complete example unchanged. Separate the exported helper into select-topics.mjs. Import it from a server file and a test file. Use the response status and JSON body as your acceptance evidence.

Before you start

Install Node.js 22.12+ (or a supported newer LTS). Create a folder named codingneed-node. Open a terminal in that folder. Save ONE lesson example as lesson.mjs and run node lesson.mjs. No npm dependencies are required. HTTP lessons keep running until you press Ctrl+C; stop the previous server before starting another.

File for this example: lesson.mjs

New words, explained

serialization
Converting values into a transport format such as JSON.
entryPoint
The file used to start the program.
sideEffect
An observable action such as opening a server or writing a file.

Follow the example step by step

  1. Run the complete example unchanged.
  2. Separate the exported helper into select-topics.mjs.
  3. Import it from a server file and a test file.
  4. Use the response status and JSON body as your acceptance evidence.

You are ready to move on when: The helper can be imported without starting a server. Valid, empty-result, invalid-limit and unknown-route requests behave as documented. Tests check filtering, limits and input preservation. Explain which features would be needed before accepting private user data.

Read the example

import {createServer} from 'node:http';
const topics = [{id:1, title:'React'}, {id:2, title:'SQL'}, {id:3, title:'Node.js'}];
export function selectTopics(items, query, limit) {
  return items.filter(item => item.title.toLowerCase().includes(query.trim().toLowerCase())).slice(0, limit);
}
createServer((req, res) => {
  function send(status, body) { res.writeHead(status, {'Content-Type':'application/json'}); res.end(JSON.stringify(body)); }
  try {
    const url = new URL(req.url, 'http://localhost');
    if (req.method !== 'GET' || url.pathname !== '/topics') { send(404, {error:'Not found'}); return; }
    const q = url.searchParams.get('q') ?? '', raw = url.searchParams.get('limit') ?? '3';
    if (q.length > 40 || !/^[1-3]$/.test(raw)) { send(400, {error:'Use q up to 40 characters and limit from 1 to 3'}); return; }
    const items = selectTopics(topics, q, Number(raw));
    send(200, {items, count:items.length});
  } catch { send(400, {error:'Invalid request'}); }
}).listen(3000, '127.0.0.1', () => console.log('Open http://127.0.0.1:3000/topics'));
Check the expected output
/topics?q=sql&limit=1 returns {"items":[{"id":2,"title":"SQL"}],"count":1}. An unmatched search returns items:[] and count:0.

Your challenge

Move selectTopics into a separate module without server startup side effects. Add the behavior tests from the previous lesson, then document three example requests and their status codes.

Solution cost: Discuss the operations in this small example; rendering and I/O costs depend on the host. time · Proportional to the example’s retained data. space

Common trap

An imported module runs top-level code. Keep listen in the entry file, not the reusable helper.

Study the project implementation
import {createServer} from 'node:http';
const topics = [{id:1, title:'React'}, {id:2, title:'SQL'}, {id:3, title:'Node.js'}];
export function selectTopics(items, query, limit) {
  return items.filter(item => item.title.toLowerCase().includes(query.trim().toLowerCase())).slice(0, limit);
}
createServer((req, res) => {
  function send(status, body) { res.writeHead(status, {'Content-Type':'application/json'}); res.end(JSON.stringify(body)); }
  try {
    const url = new URL(req.url, 'http://localhost');
    if (req.method !== 'GET' || url.pathname !== '/topics') { send(404, {error:'Not found'}); return; }
    const q = url.searchParams.get('q') ?? '', raw = url.searchParams.get('limit') ?? '3';
    if (q.length > 40 || !/^[1-3]$/.test(raw)) { send(400, {error:'Use q up to 40 characters and limit from 1 to 3'}); return; }
    const items = selectTopics(topics, q, Number(raw));
    send(200, {items, count:items.length});
  } catch { send(400, {error:'Invalid request'}); }
}).listen(3000, '127.0.0.1', () => console.log('Open http://127.0.0.1:3000/topics'));

Further reading: Official documentation

Essential cookies keep your account signed in. Optional analytics is not configured on this site. Your choice does not affect access to lessons.

Read the Privacy Policy. You can change this choice in the footer.