Use Cases — project
Behavioral contracts of the implemented RealWorld Conduit REST API. Derived from the shipped implementation (
src/http,src/services) and the external Hurl conformance suite (evidence/hurl, 13/13 green). One UC per endpoint group. Each UC carries machine-checkable Acceptance Criteria.
Conventions (apply to every UC)
- Auth header:
Authorization: Token <jwt>(RealWorld) orBearer <jwt>(compatibility). JWT is HS256, payload{ userId }. - Optional auth: a missing / malformed / expired / invalid token serves the anonymous view (never
401); viewer-relative flags (following,favorited) are thenfalse. - Error envelope: every error body is
{ "errors": { "<key>": ["<msg>", …] } }. Validation (422) is field-keyed (envelope prefixuser/article/commentstripped); other errors are resource-scoped (token,credentials,profile,article,comment,email,username). - Profile object (embedded as
authorand returned by profile endpoints):{ username, bio, image, following }—bio/imagenullable.
UC-001: Register and Log In
Actor: API consumer (unauthenticated client) Precondition: The system is operational. Steps:
- Actor
POST /api/userswith{ user: { username, email, password } }. - System validates fields, ensures email and username are unique, hashes the password (Argon2id), persists the user, and issues a JWT.
- Actor later
POST /api/users/loginwith{ user: { email, password } }. - System verifies the credentials and issues a fresh JWT. Postcondition: A user account exists; the Actor holds a JWT identifying it. Register returns
201; login returns200. Both return{ user: { email, token, username, bio, image } }(bio/imagenullfor a new account). Error Cases:- Register — email already taken →
409{ errors: { email: ['has already been taken'] } }. - Register — username already taken →
409{ errors: { username: ['has already been taken'] } }. - Register — missing/blank/invalid field (email format, password < 8 chars) →
422field-keyed (e.g.{ errors: { email: ['is invalid'] } }). - Login — unknown email OR wrong password →
401{ errors: { credentials: ['invalid'] } }(identical message; no account enumeration). - Login — missing/blank field →
422. Acceptance Criteria: - AC-001.1:
POST /api/users(no auth) with a valid unique user ⇒201; responseuser.tokenis a non-empty string;user.bio === nullanduser.image === null. - AC-001.2: A second
POST /api/usersreusing the same email ⇒409with body exactly{ errors: { email: ['has already been taken'] } }. - AC-001.3:
POST /api/userswithpasswordof length 7 ⇒422and responseerrors.passwordis a non-empty array. - AC-001.4:
POST /api/users/loginwith the registered email + correct password ⇒200with auser.token. - AC-001.5:
POST /api/users/loginwith a wrong password ⇒401with body exactly{ errors: { credentials: ['invalid'] } }.
- Register — email already taken →
UC-002: View and Update the Current User
Actor: Authenticated user Precondition: Actor holds a valid JWT for an existing account. Steps:
- Actor
GET /api/userwith the auth header. - System resolves the token to the account and returns it.
- Actor
PUT /api/userwith{ user: { … } }containing one or more ofemail,username,password,bio,image. - System validates, enforces email/username uniqueness against other accounts, applies the partial update, and returns the account. Postcondition: Both calls return
200with{ user: { email, token, username, bio, image } }. The token is unchanged byPUT. Blank/whitespace-onlybio/imagecoerce tonull. Error Cases:- Missing token →
401{ errors: { token: ['is missing'] } }. - Invalid/expired token or unsupported scheme →
401. PUTwith an emptyuserobject (no fields) →422.PUTchanging email/username to one owned by another account →409.PUTwith an invalid field value →422field-keyed. Acceptance Criteria:- AC-002.1:
GET /api/userwith noAuthorizationheader ⇒401with body{ errors: { token: ['is missing'] } }. - AC-002.2:
GET /api/userwith a valid token ⇒200anduser.emailmatches the authenticated account. - AC-002.3:
PUT /api/userwith{ user: { bio: 'hi' } }⇒200anduser.bio === 'hi'; other fields unchanged. - AC-002.4:
PUT /api/userwith{ user: {} }⇒422. - AC-002.5:
PUT /api/userchanging email to another user’s email ⇒409.
- Missing token →
UC-003: View a Profile and Follow / Unfollow
Actor: API consumer (GET: anonymous or authenticated; follow/unfollow: authenticated user) Precondition: A target user identified by :username exists. Steps:
- Actor
GET /api/profiles/:username(optional auth). - System returns the profile with
followingreflecting the viewer’s edge (falsewhen anonymous). - Authenticated Actor
POST /api/profiles/:username/followto follow, orDELETE /api/profiles/:username/followto unfollow. - System creates/removes the follow edge idempotently and returns the profile. Postcondition: All three endpoints return
200with{ profile: { username, bio, image, following } }. AfterPOST .../follow,following === true; afterDELETE .../follow,following === false. Follow and unfollow are idempotent (repeat ⇒ same state, no error, no duplicate edge). Self-follow is permitted (not special-cased). Error Cases:- Unknown
:username(any of the three) →404{ errors: { profile: ['not found'] } }. POST/DELETE .../followwithout auth →401. Acceptance Criteria:- AC-003.1:
GET /api/profiles/:usernamefor an existing user, anonymous ⇒200andprofile.following === false. - AC-003.2:
POST /api/profiles/:username/follow(auth) ⇒200andprofile.following === true. - AC-003.3: A second
POST .../follow⇒200, stillfollowing === true(no error, no duplicate). - AC-003.4:
DELETE /api/profiles/:username/follow⇒200andprofile.following === false. - AC-003.5:
GET /api/profiles/<nonexistent>⇒404with body{ errors: { profile: ['not found'] } }.
- Unknown
UC-004: Create, Read, Update, and Delete an Article
Actor: API consumer (GET: optional auth; create/update/delete: authenticated author) Precondition: For create, Actor is authenticated. For update/delete, the article exists and Actor is its author. Steps:
- Actor
POST /api/articleswith{ article: { title, description, body, tagList? } }. - System validates, derives a unique
slugfrom the title (slugified; on collision appends an 8-char suffix), de-duplicates/drops-blank tags, and persists the article. - Anyone
GET /api/articles/:slugto read the full article (optional auth). - Author
PUT /api/articles/:slugwith a partial{ article: { … } }; iftitlechanges the slug is re-derived. - Author
DELETE /api/articles/:slugto soft-delete it. Postcondition: Create ⇒201; get/update ⇒200, each returning{ article: { slug, title, description, body, tagList, createdAt, updatedAt, favorited, favoritesCount, author } }(single-article responses includebody). Delete ⇒204with an empty body; the article is soft-deleted (deletedAtset) and excluded from all subsequent reads (GET⇒404). On create,favorited === false,favoritesCount === 0,createdAt === updatedAt. Error Cases:- Create/update/delete without auth →
401. - Create with a blank/missing required field →
422field-keyed. - Update with an empty
articleobject →422. - Update/delete by a non-author →
403{ errors: { article: ['forbidden'] } }. - Update/delete/get of an unknown or soft-deleted slug →
404. Acceptance Criteria: - AC-004.1:
POST /api/articles(auth) with a valid article ⇒201;article.slugis non-empty,article.favoritesCount === 0,article.favorited === false, andarticle.bodyis present. - AC-004.2: Two articles created with the same title produce two distinct
slugvalues. - AC-004.3:
GET /api/articles/:slugfor an existing article ⇒200and the responsearticleincludes abodyfield. - AC-004.4:
PUT /api/articles/:slugby a non-author ⇒403with body{ errors: { article: ['forbidden'] } }. - AC-004.5:
DELETE /api/articles/:slugby the author ⇒204with an empty body; a subsequentGET /api/articles/:slug⇒404. - AC-004.6:
POST /api/articleswithout auth ⇒401.
- Create/update/delete without auth →
UC-005: List Articles, Feed, and Pagination
Actor: API consumer (list: optional auth; feed: authenticated user) Precondition: Zero or more non-deleted articles exist. Steps:
- Actor
GET /api/articleswith optional filterstag,author,favorited, and paginationlimit/offset. - System returns the matching page, newest first, with the pre-pagination total.
- Authenticated Actor
GET /api/articles/feedto get articles by followed authors, with the same pagination. Postcondition: Both return200with{ articles: [...], articlesCount }. List/feed article items omit thebodyfield (performance contract). Order is newest first (createdAtdesc, insertion-sequence tie-break).limitdefaults to20(range 1–100),offsetdefaults to0.articlesCountis the total matching count before pagination. Filters combine conjunctively;author/favoritednaming a non-existent user yields an empty page (articlesCount === 0), not404. The feed of a user following no one is an empty page. Error Cases:GET /api/articles/feedwithout auth →401.limit/offsetout of range or non-numeric →422. Acceptance Criteria:- AC-005.1:
GET /api/articles⇒200; each item inarticleshas nobodyfield; response has an integerarticlesCount. - AC-005.2: With > 20 articles,
GET /api/articlesreturns at most 20 items andarticlesCountequals the full matching total. - AC-005.3:
GET /api/articles?limit=1returns exactly 1 item (when ≥1 match);articles[0]is the most recently created matching article. - AC-005.4:
GET /api/articles?author=<nonexistent>⇒200witharticles === []andarticlesCount === 0. - AC-005.5:
GET /api/articles/feedwithout auth ⇒401; with auth ⇒200containing only articles authored by users the viewer follows.
UC-006: Favorite and Unfavorite an Article
Actor: Authenticated user Precondition: Actor is authenticated and the target article exists. Steps:
- Actor
POST /api/articles/:slug/favoriteto favorite. - System records the favorite edge idempotently and recomputes the count.
- Actor
DELETE /api/articles/:slug/favoriteto unfavorite. Postcondition: Both return200with the full article{ article: { …, favorited, favoritesCount, … } }(body included). After favorite,favorited === trueandfavoritesCountreflects the increment; after unfavorite,favorited === falseand the count reflects the decrement.favoritesCountis derived, not stored. Operations are idempotent (re-favorite / re-unfavorite ⇒ same state, count unchanged, no error). Error Cases:- Without auth →
401. - Unknown/soft-deleted slug →
404. Acceptance Criteria: - AC-006.1:
POST /api/articles/:slug/favorite(auth) ⇒200,article.favorited === true,article.favoritesCount === 1(from 0). - AC-006.2: A second
POST .../favorite⇒200,favorited === true,favoritesCountstill1(idempotent). - AC-006.3:
DELETE /api/articles/:slug/favorite⇒200,favorited === false,favoritesCount === 0. - AC-006.4:
POST /api/articles/<unknown>/favorite⇒404. - AC-006.5:
POST /api/articles/:slug/favoritewithout auth ⇒401.
- Without auth →
UC-007: Comment on an Article
Actor: API consumer (list: optional auth; add/delete: authenticated user; delete: comment author only) Precondition: The article identified by :slug exists. Steps:
- Anyone
GET /api/articles/:slug/comments(optional auth) to list comments. - Authenticated Actor
POST /api/articles/:slug/commentswith{ comment: { body } }to add one. - The comment’s author
DELETE /api/articles/:slug/comments/:idto remove it. Postcondition: List ⇒200{ comments: [ { id, body, createdAt, updatedAt, author } ] }, newest first;idis an integer. Add ⇒201{ comment: { id, body, createdAt, updatedAt, author } }withcreatedAt === updatedAt. Delete ⇒204empty body; the comment is soft-deleted and excluded from subsequent reads. Error Cases:- Add/delete without auth →
401. - Add with a blank/missing
body→422. - List/add/delete on an unknown or soft-deleted
:slug→404. - Delete with an unknown
:id, a non-integer:id, or an:idthat belongs to a different article →404. - Delete by a non-author →
403{ errors: { comment: ['forbidden'] } }. Acceptance Criteria: - AC-007.1:
POST /api/articles/:slug/comments(auth) with a non-blank body ⇒201;comment.idis an integer;comment.author.following === falsefor a non-followed author. - AC-007.2:
GET /api/articles/:slug/comments⇒200;commentsordered newest first (the just-created comment iscomments[0]). - AC-007.3:
POST /api/articles/:slug/commentswith{ comment: { body: '' } }⇒422. - AC-007.4:
DELETE /api/articles/:slug/comments/:idby the author ⇒204empty body; the comment no longer appears in the list. - AC-007.5:
DELETE /api/articles/:slug/comments/:idby a non-author ⇒403with body{ errors: { comment: ['forbidden'] } }. - AC-007.6:
DELETEof a comment id under a mismatched article slug ⇒404.
- Add/delete without auth →
UC-008: List Tags
Actor: API consumer (unauthenticated or authenticated) Precondition: The system is operational. Steps:
- Actor
GET /api/tags(no auth). - System returns the distinct set of tags appearing on any non-deleted article. Postcondition: Returns
200{ tags: [...] }— the distinct, de-duplicated, alphabetically-sorted union oftagListacross all live articles. A tag drops out when its last carrying article is soft-deleted. Tags are derived from the article aggregate, not a separate store. With no tagged live articles,tags === []. Error Cases:- None — the endpoint always succeeds. Acceptance Criteria:
- AC-008.1:
GET /api/tags(no auth) ⇒200with atagsarray. - AC-008.2: A tag present on two live articles appears exactly once in
tags. - AC-008.3:
tagsis sorted in ascending alphabetical order. - AC-008.4: A tag appearing only on a soft-deleted article is absent from
tags.