# RealWorld (Conduit) — User Guide

## 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:**

```bash
HOST=http://localhost:3000 bash specs/api/run-api-tests-hurl.sh
```

```text
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 the `local` environment.

**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

```bash
cd docs
npm install
```

```text
added 450 packages in 12s
```

> [!IMPORTANT]
> The documentation site is a standalone Astro project inside the `docs/` directory with its own `package.json`. Do not run `npm install` from the repository root.

### Verifying the Docs Dev Server

```bash
cd docs
npm run dev
```

```text
🚀 astro dev server running at localhost:4321
```

### Installing Test Tooling

=== "Hurl"
    ```bash
    # macOS
    brew install hurl

    # Linux (via cargo)
    cargo install hurl
    ```

=== "Bruno CLI"
    ```bash
    npm install -g @usebruno/cli
    ```

=== "Playwright"
    ```bash
    npx playwright install --with-deps chromium
    ```

### Post-Install Verification

```bash
hurl --version
```

```text
hurl 4.3.0
```

```bash
bru --version
```

```text
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]
> `HOST` is consumed by the shell scripts `run-api-tests-hurl.sh` and `run-api-tests-bruno.sh`. `API_BASE` and `API_MODE` are consumed by the Playwright test helpers in `specs/e2e/helpers/config.ts`.

The Bruno environment file at `specs/api/bruno/environments/local.bru` sets:

```text
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:

```json
{
  "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)

```bash
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

```bash
HOST=http://localhost:3000 bash specs/api/run-api-tests-bruno.sh
```

Or run specific folders:

```bash
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 `.bru` files directly — your changes will be overwritten. Edit the `.hurl` source files instead, then regenerate with `bun specs/api/hurl-to-bruno.js`.

#### Verifying Bruno Collection is in Sync

```bash
bun specs/api/hurl-to-bruno.js --check
```

```text
Bruno collection is up to date.
```

If out of sync:

```text
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.

```bash
API_BASE=http://localhost:3000/api npx playwright test --config specs/e2e/playwright.base.ts
```

> [!IMPORTANT]
> Implementations should create their own `playwright.config.ts` that extends the base config and adds a `webServer` block to start the app automatically. See the base config at `specs/e2e/playwright.base.ts` for 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

```bash
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:**

1. Start your backend server
2. Run the Hurl test suite:
   ```bash
   HOST=http://localhost:YOUR_PORT bash specs/api/run-api-tests-hurl.sh
   ```
3. Review any failures — each test has descriptive comments explaining the expected behavior
4. 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.

```bash
bun specs/api/hurl-to-bruno.js
```

```text
Generated 145 files in api/bruno/
```

### Add a New API Test Case

**Goal:** Extend the test suite with additional validation.

1. Edit the appropriate `.hurl` file in `specs/api/hurl/`
2. Add your test with a comment header, HTTP method, assertions:
   ```text
   # Verify article slug format
   GET {{host}}/api/articles
   HTTP 200
   [Asserts]
   jsonpath "$.articles[0].slug" matches "^[a-z0-9-]+$"
   ```
3. Run to verify: `HOST=http://localhost:3000 hurl --test specs/api/hurl/articles.hurl --variable "host=http://localhost:3000" --variable "uid=$(date +%s)$$"`
4. Regenerate Bruno: `bun specs/api/hurl-to-bruno.js`

### Run E2E Tests Against a Fullstack Implementation

**Goal:** Validate both frontend and backend together.

1. Create a `playwright.config.ts` in your implementation root:
   ```typescript
   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,
     },
   });
   ```
2. Copy or symlink the `specs/e2e/` directory into your project
3. Run: `npx playwright test`

> [!TIP]
> Set `API_MODE=false` to test fullstack implementations that don't expose a separate API. This skips tests that use `page.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:

```typescript
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

```bash
cd assets/media
bun conduit-logo.svg.generate.ts
```

```text
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:**

1. Ensure your backend starts with a clean database for each test run, or
2. Verify that `generateUniqueUser()` produces unique timestamps (it uses `Date.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:**

```bash
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: false` and `workers: 1` settings 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:**

1. Check that your backend is running and reachable at the configured `API_BASE`
2. Verify your registration/login endpoint returns the expected `{ "user": { "token": "..." } }` envelope
3. Check for duplicate username/email if registering

**Prevention:** Use `generateUniqueUser()` which generates timestamp-based unique credentials.

> [!CAUTION]
> The default `API_BASE` points to `https://api.realworld.show/api` — the live demo server. If you're testing a local implementation, always set `API_BASE` explicitly 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`, not `Bearer`. This is a deliberate RealWorld convention. Implementations must accept the `Token` prefix 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.

---

_Generated May 2026 from realworld-apps/realworld at e75fef3. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
