🔌 API testing playground
Practice REST API
A fake shop API with logins, products, orders, paging, validation errors, idempotency, ETags and rate limits — plus a buggy mode with 8 hidden bugs to find. It runs on your own computer: free, unlimited, works with every API tool, and safe to load test.
One file · no dependencies · Node.js 20 or newer · fictional data · nothing is sent to our servers.
Start in 2 minutes
Step 1
Download the server
You need Node.js 20+. Check with
node -v.Version 1.0.1 · Is it safe? · MIT licenceVerify the download (optional)
Your copy should have this SHA-256 fingerprint:
470d9b1eb4c531fcbf516230182de221c4e7cc46694375305f46fc1a44353be3Check it with
shasum -a 256 practiceqa-api.mjs(macOS/Linux) orGet-FileHash practiceqa-api.mjs(Windows PowerShell).Step 2
Run it
In a terminal, in the folder where you saved it:
node practiceqa-api.mjs # with the 8 hidden bugs switched on: node practiceqa-api.mjs --buggyOther options:
--port=4001·--token-ttl=60(seconds) ·--quiet·--help. Stop with Ctrl+C.Step 3
Start testing
- 🧪 Swagger UI: open
http://localhost:4000/docs - 📮 Postman: download the collection (21 requests with tests)
- 💻 Code: copy an example below
- ⚡ Quick try: use the ‘Try it here’ box below — no tools needed
- 🧪 Swagger UI: open
🔒 Is it safe to download?
You're right to ask before running a file from the internet. Here's exactly what it is and what it does — and how to check for yourself.
- 📄 Plain, readable code. One JavaScript file of about 640 lines — not compressed, not hidden. Read it before you run it.
- 📦 Nothing to install. No npm packages and no dependencies — it uses only Node.js's own built-in modules. No admin rights needed.
- 🏠 Stays on your computer. It only accepts connections from your own machine (127.0.0.1 and ::1) — other devices on your network can't reach it.
- 🚫 Doesn't touch your data. It never connects to the internet or to our servers, never reads or changes your files, and never runs other programs.
- 🧪 Fictional data only. Products, orders and users are made up and kept in memory. Stop it with Ctrl+C and nothing is left behind — delete the file to remove it completely.
- ✅ Check it yourself. Compare its fingerprint (above, under “Verify the download”) and watch every request it receives in the terminal while it runs.
One note: the Swagger page at /docs loads its viewer from the jsDelivr CDN in your browser — the server itself never goes online. The file is free and open under the MIT licence (included at the top of the file): use it, change it and share it, keeping the copyright notice. It's practice software provided as-is — please don't put real personal data into it.
Test accounts
| Username | Password | Role | Can |
|---|---|---|---|
| admin | Admin@123 | Admin | Everything: create, edit and delete products, see all orders and users |
| ravi | Ravi@123 | Customer | Browse, place and cancel own orders (has order #1) |
| meera | Meera@123 | Customer | Browse, place and cancel own orders (has orders #2 and #3) |
Try it here
Start the server first, then send requests from this page. Your browser may ask permission to connect to devices on your local network — that's your own computer, so allow it (or use Postman).
This console sends requests from your browser straight to http://localhost:4000. Nothing goes to our servers.
Endpoints
Base URL http://localhost:4000. Every endpoint also accepts an X-Delay header (0–10000 ms) to practice timeouts. Full details in Swagger at /docs.
| Method | Path | Login | What it does | Status codes |
|---|---|---|---|---|
| POST | /api/auth/login | — | Log in, get a Bearer token | 200 · 400 · 401 · 415 · 429 |
| GET | /api/auth/me | Any user | Who am I? | 200 · 401 |
| POST | /api/auth/logout | Any user | Invalidate the token | 204 · 401 |
| GET | /api/products | — | Search, filter, sort and page | 200 · 400 |
| GET | /api/products/{id} | — | One product (ETag, 304) | 200 · 304 · 404 |
| POST | /api/products | Admin | Create a product | 201 · 401 · 403 · 409 · 422 |
| PUT | /api/products/{id} | Admin | Replace a product (If-Match) | 200 · 404 · 412 · 422 |
| PATCH | /api/products/{id} | Admin | Change some fields | 200 · 404 · 412 · 422 |
| DELETE | /api/products/{id} | Admin | Delete a product | 204 · 404 · 412 |
| GET | /api/orders | Any user | Your orders (admin: all), ?status= | 200 · 400 · 401 |
| POST | /api/orders | Any user | Place an order (Idempotency-Key) | 201 · 401 · 409 · 422 |
| GET | /api/orders/{id} | Any user | One of your orders | 200 · 403 · 404 |
| POST | /api/orders/{id}/cancel | Any user | Cancel a PLACED order | 200 · 403 · 404 · 409 |
| GET | /api/users | Admin | All users | 200 · 401 · 403 |
| GET | /api/health | — | Running? Clean or buggy? | 200 |
| POST | /api/reset | — | Restore the starting data | 200 |
Code examples
Every example here is run against the practice server in our test suite.
tests/api.spec.ts · npx playwright test tests/api.spec.ts
import { test, expect } from '@playwright/test';
test.use({ baseURL: 'http://localhost:4000' });
test.beforeEach(async ({ request }) => {
await request.post('/api/reset');
});
test('a customer can place an order', async ({ request }) => {
const login = await request.post('/api/auth/login', {
data: { username: 'ravi', password: 'Ravi@123' },
});
expect(login.status()).toBe(200);
const { token } = await login.json();
const res = await request.post('/api/orders', {
headers: { Authorization: `Bearer ${token}` },
data: { items: [{ productId: 1, quantity: 2 }] },
});
expect(res.status()).toBe(201);
const order = await res.json();
expect(order.status).toBe('PLACED');
expect(order.total).toBe(799 * 2);
});
test('products can be sorted by price, highest first', async ({ request }) => {
const res = await request.get('/api/products', { params: { sort: '-price', limit: 100 } });
const prices = (await res.json()).data.map((p: { price: number }) => p.price);
expect(prices).toEqual([...prices].sort((a, b) => b - a));
});Check the UI against an API
Nova Shop and TrustBank each have a small online API that uses the same rules as their screens. Use them for hybrid tests: do something in the UI, ask the API the same question, and assert the two agree. They store nothing and allow 30 requests a minute — for load testing, use the local server above.
| Method | Path | App | What it does | Status codes |
|---|---|---|---|---|
| GET | /api/shop/products | Nova Shop | Catalogue, coupons, shipping prices and tax rate | 200 |
| POST | /api/shop/quote | Nova Shop | Prices a cart: lines, subtotal, discount, shipping, tax and total | 200 · 400 · 404 · 409 · 413 · 422 · 429 |
| GET | /api/bank/rules | TrustBank | Daily limit, RTGS minimum, approval threshold and opening balances | 200 |
| POST | /api/bank/transfer-check | TrustBank | Validates a transfer and says whether it needs approval | 200 · 400 · 422 · 429 |
import { test, expect } from '@playwright/test';
test('cart totals in the UI match the quote API', async ({ page, request }) => {
// UI: sign in, add two products, apply a coupon
await page.goto('/shop');
await page.getByTestId('username').fill('demo_user');
await page.getByTestId('password').fill('Demo@123');
await page.getByTestId('login-button').click();
await page.getByTestId('add-to-cart-p1').click();
await page.getByTestId('add-to-cart-p4').click();
await page.getByTestId('cart-link').click();
await page.getByTestId('coupon-input').fill('SAVE10');
await page.getByTestId('apply-coupon').click();
// API: ask for the same cart
const res = await request.post('/api/shop/quote', {
data: { items: [{ id: 'p1', qty: 1 }, { id: 'p4', qty: 1 }], coupon: 'SAVE10' },
});
expect(res.status()).toBe(200);
const quote = await res.json();
// The UI must show exactly what the API calculated
const usd = (n: number) => '$' + n.toFixed(2);
await expect(page.getByTestId('subtotal')).toHaveText(usd(quote.subtotal));
await expect(page.getByTestId('discount')).toHaveText('−' + usd(quote.discount));
await expect(page.getByTestId('tax')).toHaveText(usd(quote.tax));
await expect(page.getByTestId('total')).toHaveText(usd(quote.total));
});
test('the transfer form and the API reject the same amount', async ({ request }) => {
const res = await request.post('/api/bank/transfer-check', {
data: { from: 'savings', amount: 60000, mode: 'imps' },
});
expect(res.status()).toBe(422);
expect((await res.json()).errors.amount).toBe('Daily transfer limit is ₹50,000');
});What to practice
Beginner
- Status codes & bodyGET /api/products: check 200, the page fields and that X-Total-Count equals total.
- Log in and reuse the tokenSave the token from /api/auth/login and send it as Authorization: Bearer … on /api/auth/me.
- Negative testsWrong password (401), missing fields (400), no token (401), customer creating a product (403), unknown id (404).
Intermediate
- Query parametersCombine search, category, minPrice/maxPrice, inStock and sort — and assert every item matches.
- PaginationWalk all pages and prove every product appears exactly once.
- Validation errorsSend several bad fields at once and assert every one is listed in error.details.
- Schema validationDownload /openapi.json and validate responses against it (Ajv, Rest Assured's JSON schema validator, jsonschema in Python).
- End-to-end flowLog in → place an order → check the total and the stock drop → cancel → check the stock comes back.
Advanced
- IdempotencySend the same order twice with one Idempotency-Key: one order, stock reduced once, Idempotent-Replayed: true.
- Optimistic lockingGET a product's ETag, change it once, then PUT with the old ETag in If-Match — expect 412.
- CachingSend If-None-Match with the ETag you received — expect 304 and no body.
- Timeouts & retriesAdd X-Delay: 3000 and set a 1-second client timeout. Does your framework fail cleanly and retry safely?
- Rate limitingFail a login 5 times, then read Retry-After from the 429 and wait that long.
- Token expiryStart with --token-ttl=5 and make your framework log in again when it gets TOKEN_EXPIRED.
- Load testRun the k6 script below against your own server — it's on your computer, so push it as hard as you like.
Buggy mode
Restart with node practiceqa-api.mjs --buggy. Write tests that pass in normal mode and fail in buggy mode — one for each bug. The Postman collection marks the requests that catch them with [B1]…[B8].
🐞 8 hidden bugs
- B1 Creating a product returns the wrong status code
- B2 Invalid prices are accepted
- B3 Paging repeats an item
- B4 A list leaks sensitive data
- B5 Customers can see other customers' orders
- B6 Order totals are wrong
- B7 Expired tokens still work
- B8 Descending sort doesn't sort descending
Good to know
- Why run it on my computer?
- It's free and unlimited, it works with every tool (Postman, Rest Assured, Python, k6…), you can load test it without harming anyone, and your data never leaves your machine.
- How do I start fresh?
- POST /api/reset restores the starting data and logs everyone out. Restarting the server does the same.
- Port 4000 is busy
- Run node practiceqa-api.mjs --port=4001 and use that port everywhere (the console above has a port box).
- Windows
- Use the same node command in PowerShell or Command Prompt. For curl in PowerShell, type curl.exe, or use Postman.
Preparing for interviews? See the API testing interview questions. Found a problem? Tell us.