You now have a working API but no screen. How do you know it works? You call it. This lesson shows the three levels of testing an API by hand and by script, and gives you a smoke script you will keep using, extended, until the end of the course.
Level 1: one request at a time
There are three common tools, and they all do the same job: build a request and show the response.
- curl is in every terminal.
curl -i -X POST localhost:5000/api/jobs -H "Content-Type: application/json" -d '{"company":"Acme","role":"Clerk"}'sends a POST and shows the status line and headers because of-i. It is best for quick checks and for scripts on a server. - Postman and similar apps (Insomnia, Thunder Client) give you a window with saved requests. Good for exploring someone else's API.
- The REST Client extension for VS Code reads a plain
.httpfile where each request is a few lines separated by###, and you click “Send Request”. The file is text, so you can commit it, and your whole team shares the same examples. We use it here.
### Open this file in VS Code with the "REST Client" extension and click "Send Request" above a line.
### Or copy each one into Postman / Thunder Client / curl.
@base = http://localhost:5000/api
### Is the server up?
GET {{base}}/health
### List jobs
GET {{base}}/jobs
### Add a job
POST {{base}}/jobs
Content-Type: application/json
{ "company": "Acme Logistics", "role": "Dispatcher", "status": "applied" }
### Add a job with a mistake (expect 400 and a details object)
POST {{base}}/jobs
Content-Type: application/json
{ "company": "", "status": "maybe" }
### One job
GET {{base}}/jobs/1
### Change a job
PUT {{base}}/jobs/1
Content-Type: application/json
{ "status": "interview" }
### Delete a job
DELETE {{base}}/jobs/1
### A job that does not exist (expect 404)
GET {{base}}/jobs/999
Open this file in VS Code with the REST Client extension installed. The line @base = http://localhost:5000/api defines a variable used as {{base}} in every request. Run them in order and watch the answers. Pay special attention to the one with a deliberate mistake, which should answer 400 with a details object, and to the one for a job that does not exist, which should answer 404.
Level 2: a script that checks the answers
Clicking is fine for exploring, but you want a quick way to know that nothing has broken after a change. A smoke test runs the main paths through the real running server and reports PASS or FAIL. It is called a smoke test from electronics: switch it on and see whether smoke comes out. It does not prove the app is perfect; it quickly catches the obvious breakage.
// A tiny script that calls your running API and checks the answers.
// Start the server first (npm run dev), then in another terminal: npm run smoke
const base = process.env.API_URL || 'http://localhost:5000/api';
let failed = 0;
const check = (name, ok, extra = '') => {
if (!ok) failed++;
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${ok ? '' : ' ' + extra}`);
};
const call = async (method, path, body) => {
const res = await fetch(base + path, {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
return { status: res.status, data: text ? JSON.parse(text) : null };
};
let r = await call('GET', '/health');
check('health answers 200', r.status === 200 && r.data.ok === true);
r = await call('POST', '/jobs', { company: 'Smoke Test Ltd', role: 'Tester' });
check('create answers 201', r.status === 201, JSON.stringify(r));
const id = r.data?.id ?? r.data?._id;
r = await call('POST', '/jobs', { company: '', status: 'maybe' });
check('bad body answers 400 with details', r.status === 400 && r.data.details?.company && r.data.details?.status, JSON.stringify(r));
r = await call('GET', `/jobs/${id}`);
check('read it back', r.status === 200 && r.data.company === 'Smoke Test Ltd', JSON.stringify(r));
r = await call('PUT', `/jobs/${id}`, { status: 'interview' });
check('update changes the status', r.status === 200 && r.data.status === 'interview', JSON.stringify(r));
r = await call('DELETE', `/jobs/${id}`);
check('delete answers 204', r.status === 204, JSON.stringify(r));
r = await call('GET', `/jobs/${id}`);
check('deleted job answers 404', r.status === 404, JSON.stringify(r));
console.log(failed ? `\n${failed} check(s) failed` : '\nAll checks passed');
process.exit(failed ? 1 : 0);
Read the two small helpers at the top. check(name, ok) prints a line and counts failures. call(method, path, body) wraps fetch, turns the body into JSON, and returns { status, data }, so every test line is short. At the end, process.exit(1) when any check failed gives the script a non-zero exit code, which is what automated systems use to know that something broke. In lesson 22 the script learns to sign in, and in lesson 24 you will meet real automated tests.
Level 3: automated tests
A smoke script needs a running server and checks the main flow. A real automated test suite starts its own copy of the app, uses its own database, runs dozens of small checks and runs on every change. You will build one in lesson 24. The tests will be easier to write because of how you structured the code today.
Your task
- Start the server with
npm run dev, then in a second terminal runnpm run smoke. - Break something on purpose: change the
404ingetJobto500, save, and run the smoke script. Which line fails? - Add two checks to the script:
PUTwith an invalid status answers400, andGET /api/jobs/abcanswers404.
How to check it worked
- Every line starts with PASS and the script ends with “All checks passed”.
- The deliberate break produces a FAIL line with the answer that came back.
Summary
- Use curl or a
.httpfile for single requests. - A smoke script runs the main flow and exits with a non-zero code on failure.
- Test bad input as carefully as good input.