DEV Community

azmiyuksel
azmiyuksel

Posted on

I Tested a Star Wars API With Playwright, and It Was More Fun Than I Expected

I've written a fair number of Playwright tests, but almost all of them were UI tests — clicking buttons, filling forms, waiting for a modal to show up. At some point I wanted to get better at API testing specifically, and the annoying part was finding a decent playground API to practice on. Half of the "free public APIs" out there either require a signup, have a rate limit that kicks in after ten requests, or return data so bland you forget what you were even testing five minutes later.

Then I found funapi.dev, a small collection of mock REST APIs built specifically for testing practice. No signup, no API key for the read endpoints, and — the part that got me — one of the APIs is a Star Wars-themed one with characters, planets, and starships. That's exactly the kind of nonsense I want to be querying at 11pm instead of a generic "todos" API.

So I decided to write this post as a walkthrough: setting up Playwright for API testing from scratch, then writing a handful of real tests against the Star Wars API. If you're new to API testing, or new to Playwright in general, this should get you from zero to "okay, I actually understand what's happening" in about fifteen minutes.

Why bother with an API test instead of a UI test?

Quick tangent, because I think this matters for beginners. UI tests are slow and a bit brittle — a CSS class changes and your selector breaks. API tests, on the other hand, talk directly to the backend. They're fast, they don't care what your frontend looks like, and they're great for checking things like status codes, response shapes, and edge cases (what happens when you ask for a character that doesn't exist?).

Playwright isn't just a browser automation tool — it ships with an APIRequestContext that lets you send HTTP requests and make assertions on the response, completely independent of any browser. That's what we'll be using here.

The API we're working with

The Star Wars API on funapi.dev lives at:

https://funapi.dev/api/galaxy/v1
Enter fullscreen mode Exit fullscreen mode

It covers three collections — planets, characters, and starships — and they're cross-referenced (a character has a homeworld, for example), which makes it more interesting to test than a flat list of objects. Some of the endpoints we'll touch:

  • GET /galaxy/v1/characters — list characters, with filtering and pagination
  • GET /galaxy/v1/characters/{id} — get one character
  • GET /galaxy/v1/characters/{id}/homeworld — a nested resource
  • POST /galaxy/v1/characters — requires a Bearer token
  • GET /galaxy/v1/starships/random — because why not
  • GET /galaxy/v1/planets — five planets, one of which is a swamp (you can guess which)

Everything here is a mock, so it resets and behaves predictably, which is exactly what you want when you're learning.

Setting things up

If you don't have a Playwright project yet, create one:

npm init playwright@latest
Enter fullscreen mode Exit fullscreen mode

Pick TypeScript, and when it asks whether you want GitHub Actions, that's up to you — not needed for this tutorial. Once it's done, you'll have a tests/ folder and a playwright.config.ts. We won't touch most of the config, but it's worth pointing the baseURL at the API so our tests don't repeat the full URL every time.

In playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: 'https://funapi.dev/api/galaxy/v1',
  },
});
Enter fullscreen mode Exit fullscreen mode

Now let's write some actual tests.

Test 1: listing characters

Create tests/characters.spec.ts:

import { test, expect } from '@playwright/test';

test('returns a list of characters', async ({ request }) => {
  const response = await request.get('/characters');

  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(200);

  const body = await response.json();
  expect(Array.isArray(body.results ?? body)).toBeTruthy();
});
Enter fullscreen mode Exit fullscreen mode

A couple of notes for people newer to Playwright: the request fixture is injected automatically by the test runner, you don't need to import or configure it yourself as long as baseURL is set. response.ok() is a nice shortcut instead of manually checking status() >= 200 && status() < 300.

I wrote body.results ?? body because paginated list endpoints sometimes wrap results in an object ({ results: [...], next: ... }) and sometimes just return an array — worth checking the actual shape in your browser or with a quick console.log(body) before locking in your assertion. That's a small but very real lesson from doing this myself: don't assume the shape, check it first.

Test 2: getting a single character

test('returns one character by id', async ({ request }) => {
  const response = await request.get('/characters/1');
  expect(response.status()).toBe(200);

  const character = await response.json();
  expect(character).toHaveProperty('name');
  expect(character).toHaveProperty('id', 1);
});
Enter fullscreen mode Exit fullscreen mode

This is the most basic form of an API test: hit the endpoint, check the status, check that the fields you expect are actually there. Simple, but it catches real bugs — like a field silently disappearing after a backend refactor.

Test 3: filtering by affiliation

The docs mention you can filter characters by their affiliation (rebel, empire, neutral — that kind of thing). This is a great case for testing query parameters:

test('filters characters by affiliation', async ({ request }) => {
  const response = await request.get('/characters', {
    params: { affiliation: 'rebel' },
  });

  expect(response.status()).toBe(200);

  const body = await response.json();
  const characters = body.results ?? body;

  for (const character of characters) {
    expect(character.affiliation?.toLowerCase()).toBe('rebel');
  }
});
Enter fullscreen mode Exit fullscreen mode

I like this test because it's checking behavior, not just "did the server respond." Every item in the returned list should actually match the filter — that's the kind of thing that's easy to get subtly wrong on a backend (off-by-one in a WHERE clause, case sensitivity bugs, etc.).

Worth testing the opposite case too: a filter that matches nothing shouldn't blow up with a 500 or a 404, it should just come back with an empty list and a 200. That's actually called out on the funapi.dev docs page as something specifically worth practicing, so let's write it:

test('an impossible filter returns an empty list, not an error', async ({ request }) => {
  const response = await request.get('/characters', {
    params: { affiliation: 'definitely-not-a-real-faction' },
  });

  expect(response.status()).toBe(200);

  const body = await response.json();
  const characters = body.results ?? body;
  expect(characters.length).toBe(0);
});
Enter fullscreen mode Exit fullscreen mode

This is a genuinely useful habit to build early: an empty result set is not an error, and your API (and your tests) should treat it that way.

Test 4: a nested resource

test('a character resolves to a homeworld', async ({ request }) => {
  const response = await request.get('/characters/1/homeworld');
  expect(response.status()).toBe(200);

  const homeworld = await response.json();
  expect(homeworld).toHaveProperty('name');
});
Enter fullscreen mode Exit fullscreen mode

Nested resources are a good thing to get comfortable testing, because in real-world APIs they're where a lot of bugs live — mismatched IDs, stale references, N+1 query problems on the backend that only show up when you actually chain requests together.

Test 5: checking a 404

Every API guide says "test your error cases," and almost nobody does it in practice because it feels tedious. It takes four lines:

test('a nonexistent character returns 404', async ({ request }) => {
  const response = await request.get('/characters/999999');
  expect(response.status()).toBe(404);
});
Enter fullscreen mode Exit fullscreen mode

That's it. But it's these tests that catch the annoying stuff — like an endpoint that returns 200 with an empty object instead of a proper 404, which then quietly breaks your frontend's error handling six months later.

Test 6: an unauthenticated write should fail

Creating a character requires a Bearer token. Before testing that a valid token works, it's worth confirming that the endpoint actually rejects requests without one:

test('creating a character without a token is rejected', async ({ request }) => {
  const response = await request.post('/characters', {
    data: {
      name: 'Some New Character',
      affiliation: 'neutral',
    },
  });

  expect(response.status()).toBe(401);
});
Enter fullscreen mode Exit fullscreen mode

If you do have a token, testing the happy path looks like this:

test('creating a character with a valid token succeeds', async ({ request }) => {
  const response = await request.post('/characters', {
    headers: {
      Authorization: `Bearer ${process.env.GALAXY_API_TOKEN}`,
    },
    data: {
      name: 'Some New Character',
      affiliation: 'neutral',
    },
  });

  expect(response.status()).toBe(201);
  const created = await response.json();
  expect(created.name).toBe('Some New Character');
});
Enter fullscreen mode Exit fullscreen mode

Small tip: never hardcode tokens directly in your test files, even for a mock API. Get into the habit of pulling them from environment variables now, so it's automatic once you're testing something that actually matters.

Running it

npx playwright test
Enter fullscreen mode Exit fullscreen mode

Since these are pure API tests, they run fast — no browser is even launched for the request fixture tests, which is part of why API testing is such a good place to build confidence before jumping into UI automation.

Wrapping up

None of this is advanced stuff, and that's kind of the point. Status codes, response shape, filters that behave correctly on both matches and misses, nested resources, and one basic auth check — that combination covers most of what you actually need for real-world API testing. The Star Wars theme is just a nice excuse to not be bored while you practice it.

If you want to go further, funapi.dev also has pagination, rate limiting, ETags, and webhook signature scenarios set up on other mock APIs specifically for practicing those techniques — worth a look once the basics here feel comfortable.

If you write your own version of these tests, I'd genuinely like to see what you did differently — drop it in the comments.

Top comments (0)