PracticeQAAutomation logoPracticeQAAutomation

🔌 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

  1. Step 1

    Download the server

    You need Node.js 20+. Check with node -v.

    Version 1.0.1 · Is it safe? · MIT licence
    Verify the download (optional)

    Your copy should have this SHA-256 fingerprint:

    470d9b1eb4c531fcbf516230182de221c4e7cc46694375305f46fc1a44353be3

    Check it with shasum -a 256 practiceqa-api.mjs (macOS/Linux) or Get-FileHash practiceqa-api.mjs (Windows PowerShell).

  2. 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 --buggy

    Other options: --port=4001 · --token-ttl=60 (seconds) · --quiet · --help. Stop with Ctrl+C.

  3. Step 3

    Start testing

🔒 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

UsernamePasswordRoleCan
adminAdmin@123AdminEverything: create, edit and delete products, see all orders and users
raviRavi@123CustomerBrowse, place and cancel own orders (has order #1)
meeraMeera@123CustomerBrowse, 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).

Log in as

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.

MethodPathLoginWhat it doesStatus codes
POST/api/auth/login—Log in, get a Bearer token200 · 400 · 401 · 415 · 429
GET/api/auth/meAny userWho am I?200 · 401
POST/api/auth/logoutAny userInvalidate the token204 · 401
GET/api/products—Search, filter, sort and page200 · 400
GET/api/products/{id}—One product (ETag, 304)200 · 304 · 404
POST/api/productsAdminCreate a product201 · 401 · 403 · 409 · 422
PUT/api/products/{id}AdminReplace a product (If-Match)200 · 404 · 412 · 422
PATCH/api/products/{id}AdminChange some fields200 · 404 · 412 · 422
DELETE/api/products/{id}AdminDelete a product204 · 404 · 412
GET/api/ordersAny userYour orders (admin: all), ?status=200 · 400 · 401
POST/api/ordersAny userPlace an order (Idempotency-Key)201 · 401 · 409 · 422
GET/api/orders/{id}Any userOne of your orders200 · 403 · 404
POST/api/orders/{id}/cancelAny userCancel a PLACED order200 · 403 · 404 · 409
GET/api/usersAdminAll users200 · 401 · 403
GET/api/health—Running? Clean or buggy?200
POST/api/reset—Restore the starting data200

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

Playwright
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.

MethodPathAppWhat it doesStatus codes
GET/api/shop/productsNova ShopCatalogue, coupons, shipping prices and tax rate200
POST/api/shop/quoteNova ShopPrices a cart: lines, subtotal, discount, shipping, tax and total200 · 400 · 404 · 409 · 413 · 422 · 429
GET/api/bank/rulesTrustBankDaily limit, RTGS minimum, approval threshold and opening balances200
POST/api/bank/transfer-checkTrustBankValidates a transfer and says whether it needs approval200 · 400 · 422 · 429
hybrid.spec.ts
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

  1. B1 Creating a product returns the wrong status code
  2. B2 Invalid prices are accepted
  3. B3 Paging repeats an item
  4. B4 A list leaks sensitive data
  5. B5 Customers can see other customers' orders
  6. B6 Order totals are wrong
  7. B7 Expired tokens still work
  8. 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.