# RealWorld Conduit API — One-Shot Benchmark An AI coding agent built the [RealWorld](https://github.com/realworld-apps/realworld) Conduit API in a **single shot** — no retries, no human intervention during execution. ## Result ``` Executed files: 13 Executed requests: 149 (122.8/s) Succeeded files: 13 (100.0%) Failed files: 0 (0.0%) Duration: 1213 ms ``` 13 Hurl test files, 149 requests, **100% pass**. One shot. 7 minutes 32 seconds wall time. ## How it was done The agent was given: - The OpenAPI 3.1 spec (`specs/api/openapi.yml`) - 14 Hurl test files as the success criteria - A fixed stack (TypeScript, Express, Prisma, SQLite) The [engineering-discipline](https://github.com/tmdgusya/engineering-discipline) harness drove execution through: **clarification** (spec analysis + edge case extraction) → **plan-crafting** (dependency-ordered implementation plan) → **run-plan** (worker-validator execution with incremental test verification). The full session transcript is included: [`transcript.txt`](transcript.txt) ## What was built 19 API endpoints across 6 domains: | Domain | Endpoints | |--------|-----------| | Auth | `POST /users`, `POST /users/login`, `GET /user`, `PUT /user` | | Profiles | `GET /profiles/:username`, `POST /profiles/:username/follow`, `DELETE /profiles/:username/follow` | | Articles | `GET /articles`, `GET /articles/feed`, `POST /articles`, `GET /articles/:slug`, `PUT /articles/:slug`, `DELETE /articles/:slug` | | Comments | `GET /articles/:slug/comments`, `POST /articles/:slug/comments`, `DELETE /articles/:slug/comments/:id` | | Favorites | `POST /articles/:slug/favorite`, `DELETE /articles/:slug/favorite` | | Tags | `GET /tags` | ## Implementation details ### Architecture ``` src/ ├── index.ts # Express app, route mounting, Prisma client ├── middleware/auth.ts # JWT auth (requireAuth / optionalAuth) ├── utils/errors.ts # Error response helpers (401/403/404/409/422) └── routes/ ├── users.ts # 127 lines — register, login, get, update ├── profiles.ts # 89 lines — get, follow, unfollow ├── articles.ts # 322 lines — CRUD, list, feed, favorite ├── comments.ts # 98 lines — create, list, delete └── tags.ts # 15 lines — list tags ``` Total: **~700 lines** of application code + 89 lines Prisma schema. ### Data model 7 Prisma models with SQLite: ``` User ─┬─< Article ─┬─< Comment │ └─< ArticleTag >─ Tag ├─< Favorite >─ Article └─< Follow (self-referential) ``` - `ArticleTag` — many-to-many join table for articles and tags - `Favorite` — composite PK `(userId, articleId)` - `Follow` — composite PK `(followerId, followingId)`, self-referential on User ### Key implementation decisions **Auth: `Token` scheme, not `Bearer`** — The RealWorld spec uses `Authorization: Token `, not the standard `Bearer` prefix. The middleware parses this with `authHeader.slice(6)`. Two variants: `requireAuth` (401 if missing) and `optionalAuth` (silently skip if missing/invalid). **Slug uniqueness** — Duplicate titles are allowed, but slugs must be unique. `uniqueSlug()` generates `title-slug`, checks for collision, appends `-1`, `-2`, etc. until unique. **Empty string → null normalization** — `PUT /user` normalizes `bio: ""` and `image: ""` to `null` before saving. The Hurl tests assert this explicitly — a subtle requirement that's easy to miss. **`tagList: null` vs `tagList: []`** — Sending `tagList: null` on article update returns 422. Sending `tagList: []` removes all tags. Omitting `tagList` entirely preserves existing tags. Three distinct behaviors for one field. **List responses omit `body`** — `GET /articles` and `GET /articles/feed` exclude the article body. Only `GET /articles/:slug` returns it. The `formatArticle` helper takes an `includeBody` flag for this. **Authorization before existence** — On article update/delete: first check if article exists (404), then check ownership (403). On comment delete: check article exists, check comment exists, then check ownership. The error hierarchy matters. **Feed = followed users only** — `GET /articles/feed` queries the Follow table for the current user's followed IDs, then filters articles by `authorId IN (followedIds)`. An empty follow list returns an empty feed, not all articles. ## Stack - TypeScript + Express + Prisma + SQLite - 8 source files, 7 Prisma models - Dependencies: `express`, `@prisma/client`, `bcryptjs`, `jsonwebtoken`, `slugify` ## Run ```bash npm install npx prisma generate npx prisma migrate dev --name init npx ts-node src/index.ts ``` Server starts on `http://localhost:8000`. ## Verify Clone the [RealWorld repo](https://github.com/realworld-apps/realworld) and run: ```bash HOST=http://localhost:8000 bash specs/api/run-api-tests-hurl.sh ``` ## License MIT