Challenge 3: Trace a Full Mutation Lifecycle — Possible Solution ==================================================================== CALL: An authenticated client sends createPost(input: { title: "" }) STEP-BY-STEP TRACE -------------------- 1. The client's HTTP POST request to /graphql includes an Authorization: Bearer header alongside the mutation body. -> Chapter 8's authentication convention: the token travels as a normal HTTP header, exactly like the context function expects. 2. Apollo Server's context function runs for this request, extracting the token, verifying it, and returning { currentUser, userLoader, commentsByPostLoader }. -> Chapter 6's context-function mechanics, combined with Chapter 8's authentication-at-context pattern. Since the token is valid, currentUser is a real user object, not null. 3. Mutation.createPost's resolver runs, with args.input.title === "". -> Chapter 3's resolver signature: (parent, args, context) — parent is irrelevant here since this is a root Mutation field. 4. Inside the resolver, requireAuth(context) is called first and succeeds (since currentUser is a real, valid user from step 2) — execution continues rather than throwing. -> Chapter 8's reusable authorization helper pattern. 5. The resolver checks args.input.title.trim().length === 0, which is true for an empty string. -> Chapter 7's input-validation-is-a-separate-concern lesson: the schema's String! on CreatePostInput.title only guaranteed a string was PRESENT and of the right TYPE — it never guaranteed the string was non-empty, which is exactly why this additional check exists in the resolver itself. 6. Because the title is empty, the resolver returns { post: null, errors: [{ field: "title", message: "Title cannot be empty" }] } WITHOUT calling db.posts.create(...) at all — no post is actually written to the database. -> Chapter 7's payload-wrapper pattern: an expected, recoverable validation problem is communicated through the payload's own errors field, not by throwing a GraphQLError. 7. The GraphQL server serializes this result and sends back an HTTP 200 response, with data.createPost.post as null and data.createPost.errors containing the one UserError object — and critically, the TOP-LEVEL errors array in the response is empty, since nothing was thrown. -> Chapter 7's core lesson: HTTP status stays 200 regardless, and this specific kind of error lives in the payload, not the top-level errors array (which is reserved for unexpected/systemic failures, per that chapter's distinction). 8. The client, if written correctly, inspects response.data.createPost.errors (NOT just response.status or the top-level errors array) and finds the one entry, displaying "Title cannot be empty" next to the title input field. -> Chapter 7's tip box: a client that only checked HTTP status or only the top-level errors array would incorrectly treat this as a successful, empty-post creation, missing the validation message entirely. WHY THIS WORKS AS A TRACE ---------------------------- Each step names a specific mechanism introduced in a specific earlier chapter — authentication at the context level (Ch.6/Ch.8), the resolver signature (Ch.3), reusable auth helpers (Ch.8), input validation as distinct from schema-level type-checking (Ch.7), the payload-error pattern (Ch.7), and correct client-side error handling (Ch.7) — showing that even a single "failed" mutation call exercises nearly every concept this course covered, exactly as the capstone chapter intends.