# RealWorld API Testing: A Complete Guide to Hurl, Bruno, and Playwright

> Explore RealWorld API testing with Hurl, Bruno, and Playwright. Learn how these tools ensure contract validation and end-to-end UI testing for robust API development.

- Repository: [Thinkster/realworld](https://github.com/gothinkster/realworld)
- Tags: how-to-guide
- Published: 2026-02-28

---

**The RealWorld reference implementation employs a multi-layered testing strategy using Hurl for declarative API contract validation, Bruno as a GUI/CLI alternative for the same endpoints, and Playwright for end-to-end UI validation, all anchored by a canonical OpenAPI 3.1 specification.**

The `gothinkster/realworld` repository serves as the standard specification for building Medium.com clone applications, making robust RealWorld API testing essential for backend verification. The official test suite validates that any implementation correctly handles authentication, articles, comments, and profiles according to the strict OpenAPI contract. Developers can verify their backends using command-line HTTP testing tools or comprehensive browser automation.

## API Contract Validation with Hurl

**Hurl** provides the primary command-line interface for validating API conformance in the RealWorld ecosystem. The test runner located at [`specs/api/hurl/run-hurl-tests.sh`](https://github.com/gothinkster/realworld/blob/main/specs/api/hurl/run-hurl-tests.sh) orchestrates execution of all `*.hurl` files against your backend implementation.

The script discovers every `.hurl` file in the `specs/api/hurl/` directory and executes them with deterministic ordering using `hurl --jobs 1`. This single-threaded approach ensures tests run sequentially rather than in parallel, preventing race conditions in shared test data.

Execute the full Hurl suite against a local backend:

```bash

# Default host is http://localhost:8000; override with HOST variable

HOST=http://localhost:8000 ./specs/api/hurl/run-hurl-tests.sh

```

Each `.hurl` file contains declarative HTTP requests paired with assertions. The file `specs/api/hurl/tags.hurl` demonstrates the concise syntax:

```hurl
GET {{host}}/api/tags
HTTP 200
[Asserts]
jsonpath "$.tags" length > 0

```

The `{{host}}` variable injects your target URL, while JSONPath assertions verify response structure and content without requiring a programming language runtime.

## API Contract Validation with Bruno

**Bruno** offers a GUI-compatible alternative for the same API contract validation, storing requests in structured `.bru` files rather than plain text. The entry point [`specs/api/run-api-tests-bruno.sh`](https://github.com/gothinkster/realworld/blob/main/specs/api/run-api-tests-bruno.sh) launches the Bruno CLI against collections organized under `specs/api/bruno/`.

Bruno collections mirror the Hurl test coverage but provide additional workflow features like folder organization and a desktop interface for manual exploration. The runner script automatically generates a unique `UID_VAL` environment variable to isolate test data between runs.

Install the Bruno CLI and execute the collection:

```bash
npm i -g @usebruno/cli
HOST=http://localhost:8000 ./specs/api/run-api-tests-bruno.sh

```

The script passes the `HOST` and `UID_VAL` variables to the Bruno runner, which executes requests defined in files like `specs/api/bruno/tags/01-setup-register.bru`. Both Hurl and Bruno validate the same endpoints defined in [`specs/api/openapi.yml`](https://github.com/gothinkster/realworld/blob/main/specs/api/openapi.yml), allowing teams to choose based on workflow preferences.

## End-to-End Testing with Playwright

While Hurl and Bruno verify API correctness, **Playwright** validates that frontend applications correctly consume these endpoints through real browser automation. The test configuration in [`specs/e2e/playwright.base.ts`](https://github.com/gothinkster/realworld/blob/main/specs/e2e/playwright.base.ts) defines shared fixtures and timeouts for cross-browser testing.

Playwright tests spin up actual browser instances and simulate complete user flows including registration, article publication, commenting, and favorites. By default, these tests target `http://localhost:8000` for the backend API, though this can be configured via environment variables to test different implementations.

Install dependencies and run the E2E suite:

```bash
npm ci
npx playwright test

```

A typical scenario from [`specs/e2e/auth.spec.ts`](https://github.com/gothinkster/realworld/blob/main/specs/e2e/auth.spec.ts) validates the authentication flow:

```typescript
test('user can register and login', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await page.getByRole('link', { name: 'Sign up' }).click();
  await page.fill('input[placeholder="Username"]', `user${uid}`);
  await page.fill('input[placeholder="Email"]', `user${uid}@example.com`);
  await page.fill('input[placeholder="Password"]', 'Password123');
  await page.click('button:has-text("Sign up")');
  await expect(page).toHaveURL(/\/@user/);
});

```

These tests verify both API functionality and UI integration, catching issues that unit tests might miss, such as cookie handling or redirect behaviors.

## The OpenAPI Specification Foundation

All testing layers derive from the canonical contract defined in [`specs/api/openapi.yml`](https://github.com/gothinkster/realworld/blob/main/specs/api/openapi.yml), an **OpenAPI 3.1** document that specifies every endpoint, request schema, response code, and security scheme. This specification serves as the single source of truth for both automated and manual testing.

Hurl and Bruno collections map directly to operations defined in this specification, ensuring that any backend implementation passing these tests conforms to the official RealWorld API contract. The OpenAPI file also enables automatic client generation and documentation synchronization.

## Legacy Postman Support

For teams maintaining existing Postman workflows, the repository includes [`specs/api/legacy_Conduit.postman_collection.json`](https://github.com/gothinkster/realworld/blob/main/specs/api/legacy_Conduit.postman_collection.json). This JSON export provides the same request definitions as the Hurl and Bruno suites, though the project actively maintains the newer CLI-first tools as the recommended path for automated testing.

## Summary

- **Hurl tests** in `specs/api/hurl/` provide fast, deterministic CLI validation using plain-text files with JSONPath assertions executed via [`run-hurl-tests.sh`](https://github.com/gothinkster/realworld/blob/main/run-hurl-tests.sh).
- **Bruno collections** under `specs/api/bruno/` offer a GUI-compatible alternative with the same coverage, executed through [`run-api-tests-bruno.sh`](https://github.com/gothinkster/realworld/blob/main/run-api-tests-bruno.sh) with automatic test data isolation.
- **Playwright E2E tests** in `specs/e2e/` verify frontend-backend integration using real browser automation against running applications.
- All testing layers reference [`specs/api/openapi.yml`](https://github.com/gothinkster/realworld/blob/main/specs/api/openapi.yml) as the authoritative contract, ensuring consistent validation across different backend implementations.

## Frequently Asked Questions

### What is the difference between Hurl and Bruno for RealWorld API testing?

**Hurl** uses simple plain-text files with `.hurl` extensions and excels in CI/CD pipelines with minimal dependencies, while **Bruno** uses structured `.bru` files and provides both a command-line interface and a desktop GUI for interactive debugging. Both tools validate the same OpenAPI endpoints and can be used interchangeably depending on whether your workflow prioritizes text-based version control or visual request building.

### How do I run RealWorld API tests against a custom backend URL?

Both the Hurl and Bruno test scripts accept a `HOST` environment variable to target any backend implementation. For Hurl, run `HOST=https://your-api.com ./specs/api/hurl/run-hurl-tests.sh`. For Bruno, run `HOST=https://your-api.com ./specs/api/run-api-tests-bruno.sh`. Playwright E2E tests can similarly target custom hosts through configuration variables in [`playwright.base.ts`](https://github.com/gothinkster/realworld/blob/main/playwright.base.ts).

### Does the RealWorld test suite support parallel execution?

The Hurl test runner explicitly uses `--jobs 1` to enforce sequential execution and prevent test data conflicts, while Playwright runs browser tests in parallel by default across multiple workers. If you need deterministic ordering for debugging, run Playwright with `npx playwright test --workers 1`.

### Can I use these tests to validate my own RealWorld backend implementation?

Yes, any backend that correctly implements the [`specs/api/openapi.yml`](https://github.com/gothinkster/realworld/blob/main/specs/api/openapi.yml) specification can be validated using these tools. Simply start your server and point the `HOST` variable to your local or deployed instance. The tests verify exact status codes, response schemas, and error message formats required by the specification.