Skip to content
QA Knowledge Hub

HTTP basics for testers

Updated 2026-11-13 · 4 min read

Run a local HTTP lab and check methods, paths, query parameters, headers, JSON bodies and success and error responses.

A useful API bug report includes the method, URL, request headers and body, expected result, and actual response. This lab gives you those pieces without an account or a shared environment. Tested on Linux with Bash and Node v24.22.2.

1. Create an isolated lab

Run the blocks in order in one Bash terminal. Port 0 asks the operating system for an available port; the script only connects to its own server.

lab=$(mktemp -d /tmp/qa-http-XXXXXX)
cd "$lab"
node --version

Save this complete file as http-lab.mjs:

// http-lab.mjs
import http from 'node:http';
import assert from 'node:assert/strict';

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://localhost');
  const send = (status, body, headers = {}) => {
    res.writeHead(status, { 'Content-Type': 'application/json', ...headers });
    res.end(JSON.stringify(body));
  };
  if (url.pathname !== '/tickets') return send(404, { error: 'not found' });
  if (req.method === 'GET') {
    return send(200, { status: url.searchParams.get('status') ?? 'all' });
  }
  if (req.method !== 'POST') {
    return send(405, { error: 'method not allowed' }, { Allow: 'GET, POST' });
  }
  if (req.headers['content-type'] !== 'application/json') {
    return send(415, { error: 'send JSON' });
  }
  let raw = '';
  for await (const chunk of req) raw += chunk;
  let body;
  try { body = JSON.parse(raw); }
  catch { return send(400, { error: 'invalid JSON' }); }
  if (typeof body?.title !== 'string' || !body.title.trim()) {
    return send(422, { error: 'title required' });
  }
  send(201, { id: 1, title: body.title });
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const base = `http://127.0.0.1:${server.address().port}`;
async function check(method, path, headers, body, status, expected) {
  const response = await fetch(base + path, {
    method, headers, body, signal: AbortSignal.timeout(5000),
  });
  const actual = await response.json();
  assert.equal(response.status, status);
  assert.match(response.headers.get('content-type'), /application\/json/);
  assert.deepEqual(actual, expected);
  if (status === 405) assert.equal(response.headers.get('allow'), 'GET, POST');
  console.log(method, path, status, JSON.stringify(actual));
}
try {
  await check('GET', '/tickets?status=open', {}, undefined, 200, { status: 'open' });
  await check('POST', '/tickets', { 'Content-Type': 'application/json' },
    '{"title":"Fake booking issue"}', 201, { id: 1, title: 'Fake booking issue' });
  await check('POST', '/tickets', { 'Content-Type': 'application/json' },
    '{', 400, { error: 'invalid JSON' });
  await check('POST', '/tickets', { 'Content-Type': 'application/json' },
    '{}', 422, { error: 'title required' });
  await check('POST', '/tickets', { 'Content-Type': 'text/plain' },
    'hello', 415, { error: 'send JSON' });
  await check('GET', '/missing', {}, undefined, 404, { error: 'not found' });
  await check('DELETE', '/tickets', {}, undefined, 405, { error: 'method not allowed' });
} finally {
  await new Promise(resolve => server.close(resolve));
}

2. Run and interpret the responses

node http-lab.mjs

You should see seven lines with statuses 200, 201, 400, 422, 415, 404, 405, in that order. An assertion failure exits unsuccessfully. The server closes even when an assertion fails.

Request detailWhat it means in this labUseful check
MethodGET reads; POST submits a ticketUnsupported DELETE returns 405 and an Allow header
Path/tickets selects the handlerA different path returns 404
Querystatus=open supplies a filter valueThe response echoes open
HeaderContent-Type describes the sent representationtext/plain is rejected here
BodyJSON contains the titleBroken JSON and missing title produce different errors

HTTP defines the status meanings; the API contract decides which validation failure maps to which status. This example chooses 422 for valid JSON with an unusable title. Other APIs use 400. Do not file a bug solely because another service uses a different convention.

3. Understand what the green run proves

The lab checks routing and response shape. Its ticket ID is hard-coded and it stores nothing: 201 here does not prove database persistence. A production creation test should read the newly created record and verify its fields. This tiny server also accepts only the exact JSON media type string shown; it is a teaching fixture, not a production HTTP parser or secure service.

The common mistake is asserting only the status. A response can be 200 with the wrong customer's data. In an interview, explain the request contract first, then the status, headers, body and persisted state you would check. Follow-up: would retrying a POST be safe? Only if the service contract provides protection against duplicate effects; do not assume it.

The lab folder contains only this script and may be deleted after you finish. No server stays running.

Quick answers

Does a 200 response prove the feature works?

No. Check the response data and the intended side effect as well as the status.

Does this lab test HTTPS or browser CORS?

No. It uses plain HTTP on loopback. TLS and browser enforcement require separate tests.

Sources

Want a project you can show in interviews?

The portfolio project kit walks you through testing a deliberately buggy app with Selenium or Playwright and API tests, then explaining it. The app itself is free.