Node.js REST API · Delivery

Testing with API Clients

While building LearnHub you will send hundreds of requests. API clients — Postman, Insomnia, Thunder Client, VS Code REST Client and curl — make that fast, and their collections and scripts turn manual checks into repeatable tests.

Advanced 7 min Lesson 20 of 25
Before you start:Authentication
0/25 in Node.js REST API
On this page · 7 min · Advanced

Learning objectives#

  • Send requests with curl and a GUI client
  • Use environments and variables for base URLs and tokens
  • Chain requests (login → use the token)
  • Turn a collection into an automated smoke test

Explanation#

curl is everywhere: curl -i -X POST -H "Content-Type: application/json" -d "{…}" http://localhost:3000/api/courses (-i shows status and headers).

Postman / Insomnia / Thunder Client organise requests into collections with environments (local, staging) holding variables like {{baseUrl}} and {{token}}. Test scripts check status and body after each request.

VS Code REST Client uses plain .http files committed to the repo: requests separated by ###, variables with @name, and responses of named requests reusable later — great for code review.

Chain requests: the login response’s accessToken is stored in a variable and sent as Authorization: Bearer {{token}} by the following requests.

Automate it: Postman collections run in CI with Newman; or write the same flow as a short script (below) that exits with a non-zero code on failure. Use it as a smoke test after each deployment.

Syntax#

Text Illustrative
# learnhub.http — VS Code REST Client
@baseUrl = http://localhost:3000


### Login
# @name login
POST {{baseUrl}}/api/auth/login
Content-Type: application/json


{ "email": "ravi@example.com", "password": "learn-node-2026" }


### Create a course with the token from the login response
POST {{baseUrl}}/api/courses
Authorization: Bearer {{login.response.body.accessToken}}
Content-Type: application/json


{ "title": "Testing APIs", "price": 299 }

Code example: A collection as code: chained requests with assertions#

JavaScriptsmoke-test.js Illustrative
import express from 'express';


// ── The API under test (normally your real app, already running) ──
const app = express();
app.use(express.json());
const courses = [];
app.post('/api/auth/login', (req, res) =>
  req.body.password === 'learn-node-2026' ? res.json({ accessToken: 'demo-token-123' }) : res.status(401).json({ error: 'Invalid email or password' }));
app.post('/api/courses', (req, res) => {
  if (req.get('authorization') !== 'Bearer demo-token-123') return res.status(401).json({ error: 'Authentication required' });
  const course = { id: courses.length + 1, ...req.body };
  courses.push(course);
  res.status(201).location(`/api/courses/${course.id}`).json(course);
});
app.get('/api/courses/:id', (req, res) => {
  const course = courses.find(c => c.id === Number(req.params.id));
  course ? res.json(course) : res.status(404).json({ error: 'Not found' });
});


// ── The "collection": environment, requests and tests ──
const env = { baseUrl: 'http://localhost:3116' };            // like a Postman environment
let passed = 0, failed = 0;
const expect = (name, condition) => {
  condition ? passed++ : failed++;
  console.log(`  ${condition ? '✓' : '✗'} ${name}`);
};
async function send(name, method, path, { body, auth } = {}) {
  console.log(`${method} ${path}  (${name})`);
  const res = await fetch(env.baseUrl + path, {
    method,
    headers: { 'Content-Type': 'application/json', ...(auth && { Authorization: `Bearer ${env.token}` }) },
    body: body && JSON.stringify(body),
  });
  return { status: res.status, headers: res.headers, json: await res.json() };
}


const server = app.listen(3116, async () => {
  let r = await send('login', 'POST', '/api/auth/login', { body: { email: 'ravi@example.com', password: 'learn-node-2026' } });
  expect('status is 200', r.status === 200);
  expect('returns an access token', typeof r.json.accessToken === 'string');
  env.token = r.json.accessToken;                             // like pm.environment.set('token', …)


  r = await send('create course', 'POST', '/api/courses', { auth: true, body: { title: 'Testing APIs', price: 299 } });
  expect('status is 201', r.status === 201);
  expect('Location points to the new course', r.headers.get('location') === `/api/courses/${r.json.id}`);
  env.courseId = r.json.id;


  r = await send('read it back', 'GET', `/api/courses/${env.courseId}`);
  expect('title round-trips', r.json.title === 'Testing APIs');


  r = await send('create without token', 'POST', '/api/courses', { body: { title: 'Nope' } });
  expect('status is 401', r.status === 401);


  console.log(`${passed} passed, ${failed} failed`);
  process.exitCode = failed ? 1 : 0;                          // CI fails if any check fails
  server.close();
});
Expected output
POST /api/auth/login  (login)
  ✓ status is 200
  ✓ returns an access token
POST /api/courses  (create course)
  ✓ status is 201
  ✓ Location points to the new course
GET /api/courses/1  (read it back)
  ✓ title round-trips
POST /api/courses  (create without token)
  ✓ status is 401
6 passed, 0 failed

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. env plays the role of a Postman environment: base URL plus variables filled in as the run proceeds.
  2. The login response’s token is saved and used by later requests — request chaining.
  3. Each request is followed by tests on status, headers and body.
  4. A negative test (no token → 401) checks the security rule, not just the happy path.
  5. The exit code makes the script usable as a CI or post-deployment smoke test.

Real-world use case#

After every deployment of LearnHub, the pipeline runs the smoke-test collection against the new environment. If login or course creation fails, the deployment is rolled back automatically.

Common mistakes#

Hard-coding URLs and tokens

Requests that contain http://localhost:3000 and a pasted token break on every environment and expire. Use variables.

Only manual clicking

Manual checks are forgotten under pressure. Save them as a collection or script and run them automatically.

Committing secrets in collections

Exported Postman environments often contain real tokens and passwords. Keep secrets in local, uncommitted environments.

Best practices#

  • Collections or .http files in the repo
  • Environments for base URLs and secrets
  • Chain login → token → requests
  • Run the collection in CI (Newman or a script)

Remember this#

curl -i shows status + headers.

Variables: {{baseUrl}}, {{token}} — never hard-code.

A collection with tests = a smoke test.

Practice exercise#

Your task: Write curl commands that log in, store the token in a shell variable, and create a course with it.

Need a hint?
Use jq -r .accessToken to extract the token from the JSON response.
Show solution
Shell Illustrative
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"ravi@example.com","password":"learn-node-2026"}' | jq -r .accessToken)


curl -i -X POST http://localhost:3000/api/courses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"title":"Testing APIs","price":299}'

Mini quiz#

Testing with API Clients quizMultiple choice Question 1 of 3

What does curl’s -i flag do?

Score so far: 0/0

Want more? Practise all Node.js REST API MCQs · Node.js REST API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.