1. Quick Start
RealWorld is a standardized specification for building a Medium.com clone called Conduit. It defines a complete REST API, frontend templates, and end-to-end test suites so that any framework implementation can be compared apples-to-apples. The repository itself contains no application code — it provides the spec, test infrastructure, and documentation site that implementations conform to.
Key features:
- OpenAPI 3.1 specification defining 15 REST endpoints for a blogging platform
- Hurl test suite for backend API compliance validation
- Bruno test collection (auto-generated from Hurl) for interactive API exploration
- Playwright end-to-end test suite covering 13 spec areas (auth, articles, comments, social, error handling, XSS security)
- Astro/Starlight documentation site with full specification details
- Mobile icon assets for iOS, Android, watchOS, and iMessage implementations
- Conduit logo SVG generator using OpenType.js
Prerequisites: Node.js (for docs site), Bun (for test conversion tooling), Hurl or Bruno (for API tests), Playwright (for E2E tests)
Quickest way to validate a backend:
HOST=http://localhost:3000 bash specs/api/run-api-tests-hurl.sh
Running Hurl tests against http://localhost:3000 with uid=1716000000$$
Tip
If you just want to explore the API interactively, import the Bruno collection at
specs/api/bruno/into the Bruno app and select thelocalenvironment.
Next steps: Install your preferred test runner (see Installation), point it at your implementation's URL, and run the compliance suite.
2. Installation
System Requirements
| Component | Version | Purpose |
|---|---|---|
| Node.js | 18+ | Documentation site (docs/) |
| Bun | 1.0+ | Hurl-to-Bruno converter, logo generator |
| Hurl | 4.0+ | Backend API compliance tests |
| Bruno CLI | 1.0+ | Alternative API test runner |
| Playwright | Latest | Frontend E2E tests |
Installing the Documentation Site
cd docs
npm install
added 450 packages in 12s
Important
The documentation site is a standalone Astro project inside the
docs/directory with its ownpackage.json. Do not runnpm installfrom the repository root.
Verifying the Docs Dev Server
cd docs
npm run dev
🚀 astro dev server running at localhost:4321
Installing Test Tooling
Hurl
# macOS
brew install hurl
# Linux (via cargo)
cargo install hurl
Bruno CLI
npm install -g @usebruno/cli
Playwright
npx playwright install --with-deps chromium
Post-Install Verification
hurl --version
hurl 4.3.0
bru --version
1.x.x
3. Configuration
RealWorld's test infrastructure uses two environment variables to target your implementation:
| Variable | Default | Purpose |
|---|---|---|
HOST |
http://localhost:8000 (Hurl), http://localhost:3000 (Bruno) |
Base URL for API test runners |
API_BASE |
https://api.realworld.show/api |
Base URL for Playwright E2E tests |
API_MODE |
true |
When false, E2E tests skip API-only scenarios and use form-based flows |
Note
HOSTis consumed by the shell scriptsrun-api-tests-hurl.shandrun-api-tests-bruno.sh.API_BASEandAPI_MODEare consumed by the Playwright test helpers inspecs/e2e/helpers/config.ts.
The Bruno environment file at specs/api/bruno/environments/local.bru sets:
vars {
host: http://localhost:3000
}
Override it at runtime with --env-var "host=http://your-server:8080".
For the documentation site, configuration lives in docs/astro.config.mjs and docs/tsconfig.json. The TypeScript config extends astro/tsconfigs/strict with JSX support for React components:
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
See the Admin Guide for the complete configuration reference.
4. Core Usage
Running Backend API Compliance Tests
The primary use case is validating that your RealWorld backend implementation conforms to the Conduit API specification.
With Hurl (Recommended)
HOST=http://localhost:3000 bash specs/api/run-api-tests-hurl.sh
The script discovers all .hurl files in specs/api/hurl/ and runs them sequentially (--jobs 1) against the target host. Each file covers one domain:
| Test File | Coverage |
|---|---|
auth.hurl |
Registration, login, user CRUD, nullable field normalization |
articles.hurl |
Article CRUD, tag management, tag removal with empty array |
comments.hurl |
Comment CRUD, selective deletion |
favorites.hurl |
Favorite/unfavorite, filter by favorited username |
feed.hurl |
Follow-based feed, pagination (limit + offset) |
profiles.hurl |
Profile fetch (with/without auth), follow/unfollow |
tags.hurl |
Tag listing |
pagination.hurl |
Limit and offset parameters |
errors_auth.hurl |
Registration validation, login errors, auth-required endpoints |
errors_articles.hurl |
Article validation, unknown slugs, unauthorized access |
errors_authorization.hurl |
Cross-user article/comment modification (403) |
errors_comments.hurl |
Comment validation, unknown article/comment errors |
errors_profiles.hurl |
Unknown profile, unauthenticated follow |
Tip
Run a single test file to debug a specific area:
HOST=http://localhost:3000 hurl --test specs/api/hurl/auth.hurl --variable "host=http://localhost:3000" --variable "uid=$(date +%s)$$".
With Bruno
HOST=http://localhost:3000 bash specs/api/run-api-tests-bruno.sh
Or run specific folders:
bru run specs/api/bruno/auth --env local --env-var "host=http://localhost:3000"
Warning
The Bruno collection is auto-generated from the Hurl test files. Do not edit
.brufiles directly — your changes will be overwritten. Edit the.hurlsource files instead, then regenerate withbun specs/api/hurl-to-bruno.js.
Verifying Bruno Collection is in Sync
bun specs/api/hurl-to-bruno.js --check
Bruno collection is up to date.
If out of sync:
Bruno collection is out of sync with Hurl files:
changed: auth/01-register.bru
missing: auth/20-verify-username-email-update-persisted.bru
Run `bun api/hurl-to-bruno.js` to regenerate.
Running Frontend E2E Tests
The Playwright test suite validates that a frontend implementation renders correctly and handles user interactions properly.
API_BASE=http://localhost:3000/api npx playwright test --config specs/e2e/playwright.base.ts
Important
Implementations should create their own
playwright.config.tsthat extends the base config and adds awebServerblock to start the app automatically. See the base config atspecs/e2e/playwright.base.tsfor the full reference.
The E2E suite covers 13 spec areas with over 80 test cases:
| Spec File | Tests | Coverage |
|---|---|---|
auth.spec.ts |
8 | Register, login, logout, session persistence, invalid token handling |
articles.spec.ts |
10 | CRUD, favorites, tag removal, author-only edit/delete |
comments.spec.ts |
8 | Add, delete, multi-comment, auth-required, author-only delete |
navigation.spec.ts |
9 | Feed switching, tag filtering, pagination, profile tabs |
social.spec.ts |
5 | Follow/unfollow, profile views, feed from followed users |
settings.spec.ts |
— | User settings form |
error-handling.spec.ts |
18 | 400/401/403/500/network errors across all operations |
user-fetch-errors.spec.ts |
12 | App initialization error handling (4XX logout, 5XX unavailable mode) |
null-fields.spec.ts |
11 | Default avatars, null bio handling, image clearing |
url-navigation.spec.ts |
7 | URL-based feed/tag routing, query parameters |
health.spec.ts |
4 | Basic app loading, API accessibility |
xss-security.spec.ts |
12 | Image URL injection, markdown body XSS, description sanitization |
Working with the Documentation Site
cd docs
npm run dev # Dev server at localhost:4321
npm run build # Production build to ./dist/
npm run preview # Preview production build locally
Content lives in docs/src/content/docs/ as .md and .mdx files. Each file becomes a route based on its filename.
5. Common Tasks
Validate a New Backend Implementation
Goal: Confirm your backend passes the full RealWorld API specification.
Prerequisites: Your backend running and accepting requests.
Steps:
- Start your backend server
- Run the Hurl test suite:
HOST=http://localhost:YOUR_PORT bash specs/api/run-api-tests-hurl.sh - Review any failures — each test has descriptive comments explaining the expected behavior
- Fix your implementation and re-run
If it fails: Check that your API matches the OpenAPI spec at specs/api/openapi.yml. Common issues include wrong response envelope structure (e.g., { "user": {...} } wrapping) and missing nullable field handling.
Regenerate the Bruno Collection from Hurl
Goal: Update the Bruno collection after modifying Hurl test files.
bun specs/api/hurl-to-bruno.js
Generated 145 files in api/bruno/
Add a New API Test Case
Goal: Extend the test suite with additional validation.
- Edit the appropriate
.hurlfile inspecs/api/hurl/ - Add your test with a comment header, HTTP method, assertions:
# Verify article slug format GET {{host}}/api/articles HTTP 200 [Asserts] jsonpath "$.articles[0].slug" matches "^[a-z0-9-]+$" - Run to verify:
HOST=http://localhost:3000 hurl --test specs/api/hurl/articles.hurl --variable "host=http://localhost:3000" --variable "uid=$(date +%s)$$" - Regenerate Bruno:
bun specs/api/hurl-to-bruno.js
Run E2E Tests Against a Fullstack Implementation
Goal: Validate both frontend and backend together.
- Create a
playwright.config.tsin your implementation root:import { defineConfig } from '@playwright/test'; import { baseConfig } from './e2e/playwright.base'; export default defineConfig({ ...baseConfig, use: { ...baseConfig.use, baseURL: 'http://localhost:3000' }, webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 120_000, }, }); - Copy or symlink the
specs/e2e/directory into your project - Run:
npx playwright test
Tip
Set
API_MODE=falseto test fullstack implementations that don't expose a separate API. This skips tests that usepage.route()API mocking and falls back to form-based flows.
Implement the Debug Interface for Better E2E Coverage
The E2E suite uses a window.__conduit_debug__ interface for auth-state assertions:
interface ConduitDebug {
getToken: () => string | null;
getAuthState: () => 'authenticated' | 'unauthenticated' | 'unavailable' | 'loading';
getCurrentUser: () => User | null;
}
Expose this on window.__conduit_debug__ in your frontend to enable token-state validation tests.
Generate the Conduit Logo SVG
cd assets/media
bun conduit-logo.svg.generate.ts
Downloading Caudex Bold...
Written to conduit-logo.svg (147.2 x 35.8)
The script downloads the Caudex Bold font, renders "Conduit" as SVG paths using OpenType.js, and writes conduit-logo.svg.
6. Troubleshooting
Error: Unhandled hurl assert (not jsonpath)
Cause: A Hurl assertion uses a type not supported by the hurl-to-bruno converter (e.g., header, cookie, or body assertions instead of jsonpath).
Solution: The converter only handles jsonpath assertions. If your test needs non-jsonpath assertions, keep them in the Hurl file only — they won't be translated to Bruno.
Prevention: Write API assertions using jsonpath patterns when you need cross-format compatibility.
Error: Failed to register user: 409
Cause: The E2E test tried to register a user whose username or email already exists. This happens when test isolation breaks down (e.g., shared database between runs without cleanup).
Solution:
- Ensure your backend starts with a clean database for each test run, or
- Verify that
generateUniqueUser()produces unique timestamps (it usesDate.now())
Prevention: Use the provided afterEach hooks that close browser contexts between tests.
Bruno Collection Out of Sync
Cause: Someone edited a .hurl file but didn't regenerate the Bruno collection.
Solution:
bun specs/api/hurl-to-bruno.js
Prevention: Run bun specs/api/hurl-to-bruno.js --check in CI to catch drift.
E2E Tests Fail with Timeouts on Article Pages
Cause: Running 6+ Playwright tests in sequence can exhaust browser resources (network connections, file descriptors). The test suite includes 500ms–1000ms cooldown delays in afterEach hooks to mitigate this.
Solution: Ensure tests run with workers: 1 (configured in playwright.base.ts) and that browser contexts are properly closed.
Warning
The
fullyParallel: falseandworkers: 1settings in the base Playwright config are intentional. Running tests in parallel causes flaky failures due to shared backend state and resource exhaustion.
Tags Test Assertion Order Incorrect
Cause: Known issue (#839). The test expectation for tag ordering may not match the backend's actual tag return order.
Solution: Check whether your backend returns tags in insertion order or alphabetical order, and adjust the test expectation accordingly.
Invalid Email Accepted During Registration
Cause: Known issue (#1584, 8 comments). The specification does not mandate server-side email format validation, so some backends accept malformed emails.
Solution: Implement email format validation in your backend's registration endpoint if desired.
Postman Regex Rejects Valid ISO-8601 Timestamps
Cause: Known issue (#832). The legacy Postman collection's timestamp regex is too strict for valid ISO-8601 variations.
Solution: Use the Hurl or Bruno test suites instead — they use proper assertion types rather than regex matching for timestamps.
Note
The Postman collection is legacy. The canonical test suites are Hurl (source of truth) and Bruno (auto-generated from Hurl).
Error: Registration failed: or Login failed:
Cause: The Playwright auth helper received an error response from the backend. The error message from .error-messages is appended.
Solution:
- Check that your backend is running and reachable at the configured
API_BASE - Verify your registration/login endpoint returns the expected
{ "user": { "token": "..." } }envelope - Check for duplicate username/email if registering
Prevention: Use generateUniqueUser() which generates timestamp-based unique credentials.
Caution
The default
API_BASEpoints tohttps://api.realworld.show/api— the live demo server. If you're testing a local implementation, always setAPI_BASEexplicitly or you'll be running tests against the public demo.
7. FAQ
Q: Is this an application I can deploy?
No. RealWorld is a specification for building a Medium.com clone. It provides the API contract, test suites, and documentation. You need a separate implementation (backend and/or frontend) that conforms to this spec. Browse implementations at the RealWorld GitHub organization.
Q: What is the difference between Hurl and Bruno tests?
Hurl is a command-line HTTP testing tool — the .hurl files are the source of truth for API compliance tests. Bruno is an API client (like Postman) with both a GUI app and CLI runner. The Bruno collection is auto-generated from the Hurl files via specs/api/hurl-to-bruno.js. Use Hurl for CI/scripted validation; use Bruno for interactive exploration.
Q: How does authentication work in the API spec?
The API uses JWT tokens. Register via POST /api/users or login via POST /api/users/login to receive a token. Pass it in the Authorization header as Token xxxxxx.yyyyyyy.zzzzzz (not Bearer).
Important
The token scheme is
Token, notBearer. This is a deliberate RealWorld convention. Implementations must accept theTokenprefix in the Authorization header.
Q: What does API_MODE control in E2E tests?
When API_MODE is true (default), tests use direct API calls for setup (faster) and include tests that mock API responses via page.route(). When false, tests use form-based UI flows for all operations and skip API-only test scenarios. Set it to false for fullstack implementations without a separate API endpoint.
Q: How are nullable fields handled?
The spec defines bio and image as nullable (type: [string, null]). Empty strings should be normalized to null by the backend. The E2E test suite (null-fields.spec.ts) validates that frontends show a default avatar SVG when image is null/empty and never render the literal text "null" for bio.
Q: Can I use this with a backend that returns 200 instead of 204 for DELETE?
Yes. The E2E tests explicitly verify that frontends handle both 200 and 204 for delete operations, per RFC 9110 (clients should accept any 2XX as success).
Q: Where is the OpenAPI specification?
At specs/api/openapi.yml. It defines all 15 endpoints, request/response schemas, authentication, and error formats. The API version is 2.0.0 and the spec uses OpenAPI 3.1.0.
Q: How do I contribute a new test case?
See CONTRIBUTING.md in the repository root. For API tests, add to the appropriate .hurl file and regenerate Bruno. For E2E tests, create or extend a .spec.ts file in specs/e2e/, using the helpers in specs/e2e/helpers/.
Q: What is the uid variable in Hurl tests?
Each test run generates a unique uid value (from $(date +%s)$$) that's used to create unique usernames and emails. This ensures test isolation — each run creates its own users and articles without colliding with previous runs.