{"openapi":"3.0.3","info":{"title":"Mock APIs","version":"1.0.0","description":"Free REST APIs for learning, building, and testing. No API key required. Mutating requests (POST, PUT, PATCH, DELETE) are simulated and never change the shared dataset. Demo authentication is for practice only and must not be used to secure real applications.","license":{"name":"MIT"}},"servers":[{"url":"https://test.naimulhassan.me"}],"tags":[{"name":"System"},{"name":"Users"},{"name":"Products"},{"name":"Posts"},{"name":"Comments"},{"name":"Todos"},{"name":"Carts"},{"name":"Recipes"},{"name":"Quotes"},{"name":"Authentication"},{"name":"HTTP Testing"}],"paths":{"/api/v1":{"get":{"operationId":"root","summary":"API discovery","description":"Lists available resources, documentation links, and pagination defaults.","tags":["System"],"parameters":[],"responses":{"200":{"description":"API metadata and resource links.","content":{"application/json":{"schema":{"type":"object"},"example":{"name":"Mock APIs","version":"1.0.0","description":"Free REST APIs for learning, building, and testing. No API key required.","baseUrl":"http://localhost:4000/api/v1","documentation":{"openapi":"http://localhost:4000/openapi.json","swaggerUi":"http://localhost:4000/docs","website":null},"resources":[{"name":"users","url":"http://localhost:4000/api/v1/users","description":"Fictional user profiles with contact details, address, company, and role. Users own posts, todos, comments, carts, and recipes.","writable":true},{"name":"products","url":"http://localhost:4000/api/v1/products","description":"A fictional product catalog across categories with pricing, discounts, ratings, and stock levels.","writable":true},{"name":"posts","url":"http://localhost:4000/api/v1/posts","description":"Short blog-style posts written by seeded users, with tags, reactions, and view counts.","writable":true},{"name":"comments","url":"http://localhost:4000/api/v1/comments","description":"Comments left by users on posts. Every comment references an existing post and user.","writable":true},{"name":"todos","url":"http://localhost:4000/api/v1/todos","description":"Simple to-do items that belong to users. Ideal for practicing list rendering and toggling state.","writable":true},{"name":"carts","url":"http://localhost:4000/api/v1/carts","description":"Shopping carts that reference seeded users and products, with computed totals and discounts.","writable":false},{"name":"recipes","url":"http://localhost:4000/api/v1/recipes","description":"Recipes with ingredients, step-by-step instructions, timings, and nutrition information.","writable":false},{"name":"quotes","url":"http://localhost:4000/api/v1/quotes","description":"Original, fictional quotes with fictional authors. Read-only.","writable":false}],"authentication":{"login":"http://localhost:4000/api/v1/auth/login","note":"Demo-only authentication for practice. Never use it to secure real applications."},"testing":{"echo":"http://localhost:4000/api/v1/test/echo","delay":"http://localhost:4000/api/v1/test/delay?ms=1000","status":"http://localhost:4000/api/v1/test/status/404"},"limits":{"defaultPageSize":30,"maxPageSize":100,"maxDelayMs":5000,"rateLimit":{"requests":100000,"windowSeconds":60}},"notice":"POST, PUT, PATCH, and DELETE requests are simulated. They return realistic responses but never modify the shared dataset."}}}}}}},"/api/v1/users":{"get":{"operationId":"usersList","summary":"List users","description":"Returns a paginated list of users. Supports pagination, field selection, sorting, and filtering.","tags":["Users"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, firstName, lastName, username, email, phone, age, gender, birthDate, image, role, address, company.","schema":{"type":"string"},"example":"id,firstName,lastName"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","firstName","lastName","username","age","birthDate"],"default":"id"},"example":"firstName"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"role","in":"query","required":false,"description":"Filter by role.","schema":{"type":"string","enum":["admin","moderator","user"]},"example":"admin"},{"name":"gender","in":"query","required":false,"description":"Filter by gender.","schema":{"type":"string","enum":["female","male","non-binary"]},"example":"female"},{"name":"city","in":"query","required":false,"description":"Filter by city (case-insensitive exact match).","schema":{"type":"string"},"example":"Denver"},{"name":"minAge","in":"query","required":false,"description":"Minimum age (inclusive).","schema":{"type":"integer"},"example":30},{"name":"maxAge","in":"query","required":false,"description":"Maximum age (inclusive).","schema":{"type":"integer"},"example":40},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of users with `total`, `skip`, and `limit` metadata.","content":{"application/json":{"schema":{"type":"object","required":["users","total","skip","limit"],"properties":{"users":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"users":[{"id":1,"firstName":"Emily","lastName":"Carter","username":"emilyc","email":"emily.carter@example.com","phone":"+1 555-0100","age":32,"gender":"female","birthDate":"1993-02-08","image":"http://localhost:4000/api/v1/images/users/1","role":"admin","address":{"street":"3335 Lakeview Terrace","city":"Denver","state":"Colorado","postalCode":"97836","country":"United States"},"company":{"name":"Evergreen Robotics","department":"Engineering","title":"DevOps Engineer"}},{"id":2,"firstName":"Michael","lastName":"Brooks","username":"michaelb","email":"michael.brooks@example.com","phone":"+1 555-0101","age":41,"gender":"male","birthDate":"1984-09-10","image":"http://localhost:4000/api/v1/images/users/2","role":"user","address":{"street":"6332 Willow Drive","city":"Portland","state":"Oregon","postalCode":"86238","country":"United States"},"company":{"name":"Tidewater Finance","department":"Engineering","title":"Engineering Manager"}}],"total":100,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: limit must be at most 100.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/users","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"limit","message":"limit must be at most 100"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is temporarily unavailable (`SERVICE_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"usersCreate","summary":"Create a user (simulated)","description":"Validates the body and returns the user as it would be created, with the next available ID. Nothing is persisted: the shared dataset is never modified.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Users"],"parameters":[],"responses":{"201":{"description":"The simulated user, including its new `id`.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":101,"name":"Alex Morgan","email":"alex@example.com"}}}},"400":{"description":"The request body is not valid JSON (`INVALID_JSON`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The email or username is already used by a seeded user (`CONFLICT`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: email: email is required","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/users","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"email","message":"email is required"}]}}}}},"requestBody":{"required":true,"description":"Users fields.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"firstName":{"type":"string","minLength":1,"maxLength":50},"lastName":{"type":"string","minLength":1,"maxLength":50},"username":{"type":"string","pattern":"^[a-zA-Z0-9._-]{3,30}$"},"email":{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"age":{"type":"integer","minimum":1,"maximum":120},"gender":{"type":"string","enum":["female","male","non-binary"]},"phone":{"type":"string","minLength":3,"maxLength":30}},"required":["email"],"additionalProperties":false},"example":{"name":"Alex Morgan","email":"alex@example.com"}}}}}},"/api/v1/users/search":{"get":{"operationId":"usersSearch","summary":"Search users","description":"Case-insensitive search across firstName, lastName, username, email. Returns an empty `users` array with `total: 0` when nothing matches.","tags":["Users"],"parameters":[{"name":"q","in":"query","required":true,"description":"Search term (1-100 characters).","schema":{"type":"string"},"example":"emily"},{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, firstName, lastName, username, email, phone, age, gender, birthDate, image, role, address, company.","schema":{"type":"string"},"example":"id,firstName,lastName"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","firstName","lastName","username","age","birthDate"],"default":"id"},"example":"firstName"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"Matching users (possibly empty).","content":{"application/json":{"schema":{"type":"object","required":["users","total","skip","limit"],"properties":{"users":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"users":[{"id":1,"firstName":"Emily","lastName":"Carter","username":"emilyc","email":"emily.carter@example.com","phone":"+1 555-0100","age":32,"gender":"female","birthDate":"1993-02-08","image":"http://localhost:4000/api/v1/images/users/1","role":"admin","address":{"street":"3335 Lakeview Terrace","city":"Denver","state":"Colorado","postalCode":"97836","country":"United States"},"company":{"name":"Evergreen Robotics","department":"Engineering","title":"DevOps Engineer"}},{"id":56,"firstName":"Emily","lastName":"Lopez","username":"emilyl","email":"emily.lopez56@example.com","phone":"+1 555-0155","age":21,"gender":"female","birthDate":"2004-01-22","image":"http://localhost:4000/api/v1/images/users/56","role":"user","address":{"street":"6743 Elm Court","city":"Denver","state":"Colorado","postalCode":"46931","country":"United States"},"company":{"name":"Harborline Logistics","department":"Engineering","title":"Senior Software Engineer"}}],"total":2,"skip":0,"limit":2}}}},"400":{"description":"Missing or invalid `q` parameter (`INVALID_PARAMETER`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: q is required.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/users/search","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"q","message":"q is required"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}":{"get":{"operationId":"usersGet","summary":"Get a user","description":"Returns a single user by its numeric ID.","tags":["Users"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, firstName, lastName, username, email, phone, age, gender, birthDate, image, role, address, company.","schema":{"type":"string"},"example":"id,firstName,lastName"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"The user object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"},"example":{"id":1,"firstName":"Emily","lastName":"Carter","username":"emilyc","email":"emily.carter@example.com","phone":"+1 555-0100","age":32,"gender":"female","birthDate":"1993-02-08","image":"http://localhost:4000/api/v1/images/users/1","role":"admin","address":{"street":"3335 Lakeview Terrace","city":"Denver","state":"Colorado","postalCode":"97836","country":"United States"},"company":{"name":"Evergreen Robotics","department":"Engineering","title":"DevOps Engineer"}}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"User with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/users/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}},"put":{"operationId":"usersReplace","summary":"Replace a user (simulated)","description":"Validates a complete replacement body and returns the resulting user. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Users"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The replaced user.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"firstName":"Emily","lastName":"Carter-Hayes","email":"emily.hayes@example.com","age":35}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"User with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/users/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"description":"Complete user representation.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"firstName":{"type":"string","minLength":1,"maxLength":50},"lastName":{"type":"string","minLength":1,"maxLength":50},"username":{"type":"string","pattern":"^[a-zA-Z0-9._-]{3,30}$"},"email":{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"age":{"type":"integer","minimum":1,"maximum":120},"gender":{"type":"string","enum":["female","male","non-binary"]},"phone":{"type":"string","minLength":3,"maxLength":30}},"required":["email"],"additionalProperties":false},"example":{"firstName":"Emily","lastName":"Carter-Hayes","email":"emily.hayes@example.com","age":35}}}}},"patch":{"operationId":"usersPatch","summary":"Update a user (simulated)","description":"Merges the provided fields into the existing user and returns the result. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Users"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The updated user.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"firstName":"Emily","lastName":"Hayes","username":"emilyc","email":"emily.carter@example.com","phone":"+1 555-0100","age":32,"gender":"female","birthDate":"1993-02-08","image":"http://localhost:4000/api/v1/images/users/1","role":"admin","address":{"street":"3335 Lakeview Terrace","city":"Denver","state":"Colorado","postalCode":"97836","country":"United States"},"company":{"name":"Evergreen Robotics","department":"Engineering","title":"DevOps Engineer"}}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: unknownField: Unknown field(s): unknownField","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/users/1","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"unknownField","message":"Unknown field(s): unknownField"}]}}}}},"requestBody":{"required":true,"description":"Any subset of user fields.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"firstName":{"type":"string","minLength":1,"maxLength":50},"lastName":{"type":"string","minLength":1,"maxLength":50},"username":{"type":"string","pattern":"^[a-zA-Z0-9._-]{3,30}$"},"email":{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"age":{"type":"integer","minimum":1,"maximum":120},"gender":{"type":"string","enum":["female","male","non-binary"]},"phone":{"type":"string","minLength":3,"maxLength":30}},"additionalProperties":false},"example":{"lastName":"Hayes"}}}}},"delete":{"operationId":"usersDelete","summary":"Delete a user (simulated)","description":"Returns `204 No Content` if the user exists. The record is not actually removed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Users"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"204":{"description":"Empty body."},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"User with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/users/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/users/{id}/posts":{"get":{"operationId":"usersPosts","summary":"List a user's posts","description":"Returns posts whose `userId` matches the user ID. Returns 404 if the user does not exist.","tags":["Users"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, title, body, userId, tags, reactions, views.","schema":{"type":"string"},"example":"id,title,body"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","title","views","userId"],"default":"id"},"example":"title"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of posts.","content":{"application/json":{"schema":{"type":"object","required":["posts","total","skip","limit"],"properties":{"posts":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"posts":[{"id":1,"title":"Why I write tests before fixing bugs","body":"Writing the failing test first made the bug impossible to ignore. Pairing for an hour was faster than a day of back-and-forth messages. Clear error messages turned out to be the best documentation we had.","userId":1,"tags":["coding","weekend"],"reactions":{"likes":398,"dislikes":28},"views":1472},{"id":2,"title":"My slow Sunday pour-over routine","body":"Sharing a pot with neighbors turned into a weekly tradition. Fresh water and a clean kettle make a bigger difference than I expected. Weighing the beans felt fussy at first, but the cups became noticeably more consistent.","userId":1,"tags":["coffee","learning","beginners"],"reactions":{"likes":330,"dislikes":8},"views":1187}],"total":3,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The user does not exist (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"User with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/users/999999/posts","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/users/{id}/todos":{"get":{"operationId":"usersTodos","summary":"List a user's todos","description":"Returns todos whose `userId` matches the user ID. Returns 404 if the user does not exist.","tags":["Users"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, todo, completed, userId.","schema":{"type":"string"},"example":"id,todo,completed"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","todo","userId"],"default":"id"},"example":"todo"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of todos.","content":{"application/json":{"schema":{"type":"object","required":["todos","total","skip","limit"],"properties":{"todos":{"type":"array","items":{"$ref":"#/components/schemas/Todo"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"todos":[{"id":1,"todo":"Reply to project kickoff email","completed":false,"userId":1},{"id":156,"todo":"Write unit tests for the login form","completed":true,"userId":1}],"total":2,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The user does not exist (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"User with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/users/999999/todos","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/users/{id}/carts":{"get":{"operationId":"usersCarts","summary":"List a user's carts","description":"Returns carts whose `userId` matches the user ID. Returns 404 if the user does not exist.","tags":["Users"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, userId, products, total, discountedTotal, totalProducts, totalQuantity.","schema":{"type":"string"},"example":"id,userId,products"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","userId","total","discountedTotal","totalQuantity"],"default":"id"},"example":"userId"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of carts.","content":{"application/json":{"schema":{"type":"object","required":["carts","total","skip","limit"],"properties":{"carts":{"type":"array","items":{"$ref":"#/components/schemas/Cart"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"carts":[{"id":1,"userId":1,"products":[{"id":100,"title":"Aquaform Kickboard","price":117.99,"quantity":2,"total":235.98,"discountPercentage":0.09,"discountedTotal":235.77,"thumbnail":"http://localhost:4000/api/v1/images/products/100"},{"id":7,"title":"Orbitel One Smartphone","price":1192.99,"quantity":2,"total":2385.98,"discountPercentage":13.24,"discountedTotal":2070.08,"thumbnail":"http://localhost:4000/api/v1/images/products/7"},{"id":49,"title":"Lumio Watch Sport Strap","price":546.99,"quantity":4,"total":2187.96,"discountPercentage":1.02,"discountedTotal":2165.64,"thumbnail":"http://localhost:4000/api/v1/images/products/49"}],"total":4809.92,"discountedTotal":4471.49,"totalProducts":3,"totalQuantity":8}],"total":1,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The user does not exist (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"User with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/users/999999/carts","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/products":{"get":{"operationId":"productsList","summary":"List products","description":"Returns a paginated list of products. Supports pagination, field selection, sorting, and filtering.","tags":["Products"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, title, description, category, brand, price, discountPercentage, rating, stock, sku, tags, availabilityStatus, thumbnail.","schema":{"type":"string"},"example":"id,title,description"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","title","price","rating","stock","discountPercentage"],"default":"id"},"example":"title"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"category","in":"query","required":false,"description":"Filter by category slug.","schema":{"type":"string"},"example":"smartphones"},{"name":"brand","in":"query","required":false,"description":"Filter by brand (case-insensitive exact match).","schema":{"type":"string"},"example":"Lumio"},{"name":"availabilityStatus","in":"query","required":false,"description":"Filter by stock availability.","schema":{"type":"string","enum":["In Stock","Low Stock","Out of Stock"]},"example":"In Stock"},{"name":"minPrice","in":"query","required":false,"description":"Minimum price (inclusive).","schema":{"type":"number"},"example":10},{"name":"maxPrice","in":"query","required":false,"description":"Maximum price (inclusive).","schema":{"type":"number"},"example":500},{"name":"minRating","in":"query","required":false,"description":"Minimum rating (inclusive).","schema":{"type":"number"},"example":4},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of products with `total`, `skip`, and `limit` metadata.","content":{"application/json":{"schema":{"type":"object","required":["products","total","skip","limit"],"properties":{"products":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"products":[{"id":1,"title":"Lumio Nova X5 Smartphone","description":"The Lumio Nova X5 Smartphone features all-day battery life and water and dust resistance. A dependable choice from Lumio.","category":"smartphones","brand":"Lumio","price":1190.99,"discountPercentage":21.56,"rating":2.9,"stock":50,"sku":"SMA-LUM-0001","tags":["mobile","5g","electronics"],"availabilityStatus":"In Stock","thumbnail":"http://localhost:4000/api/v1/images/products/1"},{"id":2,"title":"Lumio Nova X5 Pro Smartphone","description":"The Lumio Nova X5 Pro Smartphone features water and dust resistance and a dual-lens camera system. A dependable choice from Lumio.","category":"smartphones","brand":"Lumio","price":306.99,"discountPercentage":9.03,"rating":4.4,"stock":146,"sku":"SMA-LUM-0002","tags":["mobile","5g"],"availabilityStatus":"In Stock","thumbnail":"http://localhost:4000/api/v1/images/products/2"}],"total":100,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: limit must be at most 100.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/products","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"limit","message":"limit must be at most 100"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is temporarily unavailable (`SERVICE_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"productsCreate","summary":"Create a product (simulated)","description":"Validates the body and returns the product as it would be created, with the next available ID. Nothing is persisted: the shared dataset is never modified.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Products"],"parameters":[],"responses":{"201":{"description":"The simulated product, including its new `id`.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":101,"title":"Trail Runner Water Bottle","category":"sports-accessories","brand":"Peakline","price":18.5,"stock":40}}}},"400":{"description":"The request body is not valid JSON (`INVALID_JSON`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: title: title is required; price: price is required","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/products","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"title","message":"title is required"},{"path":"price","message":"price is required"}]}}}}},"requestBody":{"required":true,"description":"Products fields.","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","minLength":1,"maxLength":1000},"category":{"type":"string","pattern":"^[a-z0-9-]{2,40}$"},"brand":{"type":"string","minLength":1,"maxLength":60},"price":{"type":"number","minimum":0,"maximum":1000000},"discountPercentage":{"type":"number","minimum":0,"maximum":100},"stock":{"type":"integer","minimum":0,"maximum":1000000},"tags":{"maxItems":10,"type":"array","items":{"type":"string","minLength":1,"maxLength":40}}},"required":["title","price"],"additionalProperties":false},"example":{"title":"Trail Runner Water Bottle","category":"sports-accessories","brand":"Peakline","price":18.5,"stock":40}}}}}},"/api/v1/products/search":{"get":{"operationId":"productsSearch","summary":"Search products","description":"Case-insensitive search across title, description, brand, category, tags. Returns an empty `products` array with `total: 0` when nothing matches.","tags":["Products"],"parameters":[{"name":"q","in":"query","required":true,"description":"Search term (1-100 characters).","schema":{"type":"string"},"example":"phone"},{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, title, description, category, brand, price, discountPercentage, rating, stock, sku, tags, availabilityStatus, thumbnail.","schema":{"type":"string"},"example":"id,title,description"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","title","price","rating","stock","discountPercentage"],"default":"id"},"example":"title"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"Matching products (possibly empty).","content":{"application/json":{"schema":{"type":"object","required":["products","total","skip","limit"],"properties":{"products":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"products":[{"id":1,"title":"Lumio Nova X5 Smartphone","description":"The Lumio Nova X5 Smartphone features all-day battery life and water and dust resistance. A dependable choice from Lumio.","category":"smartphones","brand":"Lumio","price":1190.99,"discountPercentage":21.56,"rating":2.9,"stock":50,"sku":"SMA-LUM-0001","tags":["mobile","5g","electronics"],"availabilityStatus":"In Stock","thumbnail":"http://localhost:4000/api/v1/images/products/1"},{"id":2,"title":"Lumio Nova X5 Pro Smartphone","description":"The Lumio Nova X5 Pro Smartphone features water and dust resistance and a dual-lens camera system. A dependable choice from Lumio.","category":"smartphones","brand":"Lumio","price":306.99,"discountPercentage":9.03,"rating":4.4,"stock":146,"sku":"SMA-LUM-0002","tags":["mobile","5g"],"availabilityStatus":"In Stock","thumbnail":"http://localhost:4000/api/v1/images/products/2"}],"total":18,"skip":0,"limit":2}}}},"400":{"description":"Missing or invalid `q` parameter (`INVALID_PARAMETER`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: q is required.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/products/search","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"q","message":"q is required"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/products/categories":{"get":{"operationId":"productsCategories","summary":"List product categories","description":"Returns every product category with a display name and product count.","tags":["Products"],"parameters":[{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"All categories.","content":{"application/json":{"schema":{"type":"object","properties":{"categories":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"productCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["slug","name","productCount"],"additionalProperties":false}},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["categories","total"],"additionalProperties":false},"example":{"categories":[{"slug":"audio","name":"Audio","productCount":10},{"slug":"beauty","name":"Beauty","productCount":10},{"slug":"furniture","name":"Furniture","productCount":10},{"slug":"groceries","name":"Groceries","productCount":10},{"slug":"kitchen-accessories","name":"Kitchen Accessories","productCount":10},{"slug":"laptops","name":"Laptops","productCount":10},{"slug":"smartphones","name":"Smartphones","productCount":10},{"slug":"sports-accessories","name":"Sports Accessories","productCount":10},{"slug":"tablets","name":"Tablets","productCount":10},{"slug":"wearables","name":"Wearables","productCount":10}],"total":10}}}}}}},"/api/v1/products/category/{category}":{"get":{"operationId":"productsByCategory","summary":"List products in a category","description":"Returns products in the given category. Unknown categories return 404.","tags":["Products"],"parameters":[{"name":"category","in":"path","required":true,"description":"Category slug.","schema":{"type":"string"},"example":"smartphones"},{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, title, description, category, brand, price, discountPercentage, rating, stock, sku, tags, availabilityStatus, thumbnail.","schema":{"type":"string"},"example":"id,title,description"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","title","price","rating","stock","discountPercentage"],"default":"id"},"example":"title"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of products in the category.","content":{"application/json":{"schema":{"type":"object","required":["products","total","skip","limit"],"properties":{"products":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"products":[{"id":1,"title":"Lumio Nova X5 Smartphone","description":"The Lumio Nova X5 Smartphone features all-day battery life and water and dust resistance. A dependable choice from Lumio.","category":"smartphones","brand":"Lumio","price":1190.99,"discountPercentage":21.56,"rating":2.9,"stock":50,"sku":"SMA-LUM-0001","tags":["mobile","5g","electronics"],"availabilityStatus":"In Stock","thumbnail":"http://localhost:4000/api/v1/images/products/1"},{"id":2,"title":"Lumio Nova X5 Pro Smartphone","description":"The Lumio Nova X5 Pro Smartphone features water and dust resistance and a dual-lens camera system. A dependable choice from Lumio.","category":"smartphones","brand":"Lumio","price":306.99,"discountPercentage":9.03,"rating":4.4,"stock":146,"sku":"SMA-LUM-0002","tags":["mobile","5g"],"availabilityStatus":"In Stock","thumbnail":"http://localhost:4000/api/v1/images/products/2"}],"total":10,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown category (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Product category \"time-machines\" not found. See /api/v1/products/categories","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/products/category/time-machines","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/products/{id}":{"get":{"operationId":"productsGet","summary":"Get a product","description":"Returns a single product by its numeric ID.","tags":["Products"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric product ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, title, description, category, brand, price, discountPercentage, rating, stock, sku, tags, availabilityStatus, thumbnail.","schema":{"type":"string"},"example":"id,title,description"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"The product object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Product"},"example":{"id":1,"title":"Lumio Nova X5 Smartphone","description":"The Lumio Nova X5 Smartphone features all-day battery life and water and dust resistance. A dependable choice from Lumio.","category":"smartphones","brand":"Lumio","price":1190.99,"discountPercentage":21.56,"rating":2.9,"stock":50,"sku":"SMA-LUM-0001","tags":["mobile","5g","electronics"],"availabilityStatus":"In Stock","thumbnail":"http://localhost:4000/api/v1/images/products/1"}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Product with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/products/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}},"put":{"operationId":"productsReplace","summary":"Replace a product (simulated)","description":"Validates a complete replacement body and returns the resulting product. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Products"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric product ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The replaced product.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"title":"Nova X5 Smartphone (2026 Edition)","category":"smartphones","brand":"Lumio","price":649,"stock":25}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Product with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/products/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"description":"Complete product representation.","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","minLength":1,"maxLength":1000},"category":{"type":"string","pattern":"^[a-z0-9-]{2,40}$"},"brand":{"type":"string","minLength":1,"maxLength":60},"price":{"type":"number","minimum":0,"maximum":1000000},"discountPercentage":{"type":"number","minimum":0,"maximum":100},"stock":{"type":"integer","minimum":0,"maximum":1000000},"tags":{"maxItems":10,"type":"array","items":{"type":"string","minLength":1,"maxLength":40}}},"required":["title","price"],"additionalProperties":false},"example":{"title":"Nova X5 Smartphone (2026 Edition)","category":"smartphones","brand":"Lumio","price":649,"stock":25}}}}},"patch":{"operationId":"productsPatch","summary":"Update a product (simulated)","description":"Merges the provided fields into the existing product and returns the result. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Products"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric product ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The updated product.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"title":"Lumio Nova X5 Smartphone","description":"The Lumio Nova X5 Smartphone features all-day battery life and water and dust resistance. A dependable choice from Lumio.","category":"smartphones","brand":"Lumio","price":599.99,"discountPercentage":21.56,"rating":2.9,"stock":50,"sku":"SMA-LUM-0001","tags":["mobile","5g","electronics"],"availabilityStatus":"In Stock","thumbnail":"http://localhost:4000/api/v1/images/products/1"}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: unknownField: Unknown field(s): unknownField","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/products/1","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"unknownField","message":"Unknown field(s): unknownField"}]}}}}},"requestBody":{"required":true,"description":"Any subset of product fields.","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","minLength":1,"maxLength":1000},"category":{"type":"string","pattern":"^[a-z0-9-]{2,40}$"},"brand":{"type":"string","minLength":1,"maxLength":60},"price":{"type":"number","minimum":0,"maximum":1000000},"discountPercentage":{"type":"number","minimum":0,"maximum":100},"stock":{"type":"integer","minimum":0,"maximum":1000000},"tags":{"maxItems":10,"type":"array","items":{"type":"string","minLength":1,"maxLength":40}}},"additionalProperties":false},"example":{"price":599.99}}}}},"delete":{"operationId":"productsDelete","summary":"Delete a product (simulated)","description":"Returns `204 No Content` if the product exists. The record is not actually removed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Products"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric product ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"204":{"description":"Empty body."},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Product with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/products/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/posts":{"get":{"operationId":"postsList","summary":"List posts","description":"Returns a paginated list of posts. Supports pagination, field selection, sorting, and filtering.","tags":["Posts"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, title, body, userId, tags, reactions, views.","schema":{"type":"string"},"example":"id,title,body"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","title","views","userId"],"default":"id"},"example":"title"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"userId","in":"query","required":false,"description":"Only return posts that belong to this user ID.","schema":{"type":"integer"},"example":5},{"name":"tag","in":"query","required":false,"description":"Only return posts with this tag.","schema":{"type":"string"},"example":"travel"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of posts with `total`, `skip`, and `limit` metadata.","content":{"application/json":{"schema":{"type":"object","required":["posts","total","skip","limit"],"properties":{"posts":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"posts":[{"id":1,"title":"Why I write tests before fixing bugs","body":"Writing the failing test first made the bug impossible to ignore. Pairing for an hour was faster than a day of back-and-forth messages. Clear error messages turned out to be the best documentation we had.","userId":1,"tags":["coding","weekend"],"reactions":{"likes":398,"dislikes":28},"views":1472},{"id":2,"title":"My slow Sunday pour-over routine","body":"Sharing a pot with neighbors turned into a weekly tradition. Fresh water and a clean kettle make a bigger difference than I expected. Weighing the beans felt fussy at first, but the cups became noticeably more consistent.","userId":1,"tags":["coffee","learning","beginners"],"reactions":{"likes":330,"dislikes":8},"views":1187}],"total":100,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: limit must be at most 100.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/posts","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"limit","message":"limit must be at most 100"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is temporarily unavailable (`SERVICE_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postsCreate","summary":"Create a post (simulated)","description":"Validates the body and returns the post as it would be created, with the next available ID. Nothing is persisted: the shared dataset is never modified.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Posts"],"parameters":[],"responses":{"201":{"description":"The simulated post, including its new `id`.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":101,"title":"My first API request","body":"Today I sent my first request to Mock APIs.","userId":5,"tags":[]}}}},"400":{"description":"The request body is not valid JSON (`INVALID_JSON`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: title: title is required; body: body is required; userId: userId is required","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/posts","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"title","message":"title is required"},{"path":"body","message":"body is required"},{"path":"userId","message":"userId is required"}]}}}}},"requestBody":{"required":true,"description":"Posts fields.","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"body":{"type":"string","minLength":1,"maxLength":5000},"userId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000},"tags":{"maxItems":10,"type":"array","items":{"type":"string","minLength":1,"maxLength":40}}},"required":["title","body","userId"],"additionalProperties":false},"example":{"title":"My first API request","body":"Today I sent my first request to Mock APIs.","userId":5}}}}}},"/api/v1/posts/search":{"get":{"operationId":"postsSearch","summary":"Search posts","description":"Case-insensitive search across title, body, tags. Returns an empty `posts` array with `total: 0` when nothing matches.","tags":["Posts"],"parameters":[{"name":"q","in":"query","required":true,"description":"Search term (1-100 characters).","schema":{"type":"string"},"example":"coffee"},{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, title, body, userId, tags, reactions, views.","schema":{"type":"string"},"example":"id,title,body"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","title","views","userId"],"default":"id"},"example":"title"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"Matching posts (possibly empty).","content":{"application/json":{"schema":{"type":"object","required":["posts","total","skip","limit"],"properties":{"posts":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"posts":[{"id":2,"title":"My slow Sunday pour-over routine","body":"Sharing a pot with neighbors turned into a weekly tradition. Fresh water and a clean kettle make a bigger difference than I expected. Weighing the beans felt fussy at first, but the cups became noticeably more consistent.","userId":1,"tags":["coffee","learning","beginners"],"reactions":{"likes":330,"dislikes":8},"views":1187},{"id":12,"title":"Finding the right grind size by taste","body":"I wrote down every recipe change in a small notebook for a month. Fresh water and a clean kettle make a bigger difference than I expected. Sharing a pot with neighbors turned into a weekly tradition.","userId":22,"tags":["coffee","habits"],"reactions":{"likes":834,"dislikes":22},"views":3371}],"total":10,"skip":0,"limit":2}}}},"400":{"description":"Missing or invalid `q` parameter (`INVALID_PARAMETER`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: q is required.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/posts/search","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"q","message":"q is required"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{id}":{"get":{"operationId":"postsGet","summary":"Get a post","description":"Returns a single post by its numeric ID.","tags":["Posts"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric post ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, title, body, userId, tags, reactions, views.","schema":{"type":"string"},"example":"id,title,body"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"The post object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"},"example":{"id":1,"title":"Why I write tests before fixing bugs","body":"Writing the failing test first made the bug impossible to ignore. Pairing for an hour was faster than a day of back-and-forth messages. Clear error messages turned out to be the best documentation we had.","userId":1,"tags":["coding","weekend"],"reactions":{"likes":398,"dislikes":28},"views":1472}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Post with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/posts/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}},"put":{"operationId":"postsReplace","summary":"Replace a post (simulated)","description":"Validates a complete replacement body and returns the resulting post. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Posts"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric post ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The replaced post.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"title":"Updated title","body":"Completely replaced body text.","userId":1,"tags":["update"]}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Post with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/posts/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"description":"Complete post representation.","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"body":{"type":"string","minLength":1,"maxLength":5000},"userId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000},"tags":{"maxItems":10,"type":"array","items":{"type":"string","minLength":1,"maxLength":40}}},"required":["title","body","userId"],"additionalProperties":false},"example":{"title":"Updated title","body":"Completely replaced body text.","userId":1,"tags":["update"]}}}}},"patch":{"operationId":"postsPatch","summary":"Update a post (simulated)","description":"Merges the provided fields into the existing post and returns the result. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Posts"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric post ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The updated post.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"title":"Patched title","body":"Writing the failing test first made the bug impossible to ignore. Pairing for an hour was faster than a day of back-and-forth messages. Clear error messages turned out to be the best documentation we had.","userId":1,"tags":["coding","weekend"],"reactions":{"likes":398,"dislikes":28},"views":1472}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: unknownField: Unknown field(s): unknownField","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/posts/1","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"unknownField","message":"Unknown field(s): unknownField"}]}}}}},"requestBody":{"required":true,"description":"Any subset of post fields.","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"body":{"type":"string","minLength":1,"maxLength":5000},"userId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000},"tags":{"maxItems":10,"type":"array","items":{"type":"string","minLength":1,"maxLength":40}}},"additionalProperties":false},"example":{"title":"Patched title"}}}}},"delete":{"operationId":"postsDelete","summary":"Delete a post (simulated)","description":"Returns `204 No Content` if the post exists. The record is not actually removed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Posts"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric post ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"204":{"description":"Empty body."},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Post with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/posts/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/posts/{id}/comments":{"get":{"operationId":"postsComments","summary":"List a post's comments","description":"Returns comments whose `postId` matches the post ID. Returns 404 if the post does not exist.","tags":["Posts"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric post ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, body, postId, userId, likes.","schema":{"type":"string"},"example":"id,body,postId"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","likes","postId","userId"],"default":"id"},"example":"likes"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of comments.","content":{"application/json":{"schema":{"type":"object","required":["comments","total","skip","limit"],"properties":{"comments":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"comments":[{"id":1,"body":"Do you have any recommendations for someone just starting out?","postId":1,"userId":87,"likes":9},{"id":261,"body":"Could you share more details about how you got started?","postId":1,"userId":28,"likes":50}],"total":4,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The post does not exist (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Post with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/posts/999999/comments","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/comments":{"get":{"operationId":"commentsList","summary":"List comments","description":"Returns a paginated list of comments. Supports pagination, field selection, sorting, and filtering.","tags":["Comments"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, body, postId, userId, likes.","schema":{"type":"string"},"example":"id,body,postId"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","likes","postId","userId"],"default":"id"},"example":"likes"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"postId","in":"query","required":false,"description":"Only return comments on this post ID.","schema":{"type":"integer"},"example":1},{"name":"userId","in":"query","required":false,"description":"Only return comments that belong to this user ID.","schema":{"type":"integer"},"example":5},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of comments with `total`, `skip`, and `limit` metadata.","content":{"application/json":{"schema":{"type":"object","required":["comments","total","skip","limit"],"properties":{"comments":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"comments":[{"id":1,"body":"Do you have any recommendations for someone just starting out?","postId":1,"userId":87,"likes":9},{"id":2,"body":"Sent this to a friend who will love it.","postId":2,"userId":9,"likes":29}],"total":300,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: limit must be at most 100.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/comments","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"limit","message":"limit must be at most 100"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is temporarily unavailable (`SERVICE_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"commentsCreate","summary":"Create a comment (simulated)","description":"Validates the body and returns the comment as it would be created, with the next available ID. Nothing is persisted: the shared dataset is never modified.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Comments"],"parameters":[],"responses":{"201":{"description":"The simulated comment, including its new `id`.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":301,"body":"Great write-up, thanks for sharing!","postId":1,"userId":3,"likes":0}}}},"400":{"description":"The request body is not valid JSON (`INVALID_JSON`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: body: body is required; postId: postId is required; userId: userId is required","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/comments","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"body","message":"body is required"},{"path":"postId","message":"postId is required"},{"path":"userId","message":"userId is required"}]}}}}},"requestBody":{"required":true,"description":"Comments fields.","content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":2000},"postId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000},"userId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000}},"required":["body","postId","userId"],"additionalProperties":false},"example":{"body":"Great write-up, thanks for sharing!","postId":1,"userId":3}}}}}},"/api/v1/comments/{id}":{"get":{"operationId":"commentsGet","summary":"Get a comment","description":"Returns a single comment by its numeric ID.","tags":["Comments"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric comment ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, body, postId, userId, likes.","schema":{"type":"string"},"example":"id,body,postId"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"The comment object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Comment"},"example":{"id":1,"body":"Do you have any recommendations for someone just starting out?","postId":1,"userId":87,"likes":9}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Comment with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/comments/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}},"put":{"operationId":"commentsReplace","summary":"Replace a comment (simulated)","description":"Validates a complete replacement body and returns the resulting comment. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Comments"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric comment ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The replaced comment.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"body":"Edited comment text.","postId":1,"userId":3,"likes":0}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Comment with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/comments/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"description":"Complete comment representation.","content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":2000},"postId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000},"userId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000}},"required":["body","postId","userId"],"additionalProperties":false},"example":{"body":"Edited comment text.","postId":1,"userId":3}}}}},"patch":{"operationId":"commentsPatch","summary":"Update a comment (simulated)","description":"Merges the provided fields into the existing comment and returns the result. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Comments"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric comment ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The updated comment.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"body":"Edited comment text.","postId":1,"userId":87,"likes":9}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: unknownField: Unknown field(s): unknownField","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/comments/1","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"unknownField","message":"Unknown field(s): unknownField"}]}}}}},"requestBody":{"required":true,"description":"Any subset of comment fields.","content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":2000},"postId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000},"userId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000}},"additionalProperties":false},"example":{"body":"Edited comment text."}}}}},"delete":{"operationId":"commentsDelete","summary":"Delete a comment (simulated)","description":"Returns `204 No Content` if the comment exists. The record is not actually removed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Comments"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric comment ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"204":{"description":"Empty body."},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Comment with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/comments/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/todos":{"get":{"operationId":"todosList","summary":"List todos","description":"Returns a paginated list of todos. Supports pagination, field selection, sorting, and filtering.","tags":["Todos"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, todo, completed, userId.","schema":{"type":"string"},"example":"id,todo,completed"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","todo","userId"],"default":"id"},"example":"todo"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"completed","in":"query","required":false,"description":"Filter by completion state.","schema":{"type":"boolean"},"example":"false"},{"name":"userId","in":"query","required":false,"description":"Only return todos that belong to this user ID.","schema":{"type":"integer"},"example":5},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of todos with `total`, `skip`, and `limit` metadata.","content":{"application/json":{"schema":{"type":"object","required":["todos","total","skip","limit"],"properties":{"todos":{"type":"array","items":{"$ref":"#/components/schemas/Todo"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"todos":[{"id":1,"todo":"Reply to project kickoff email","completed":false,"userId":1},{"id":2,"todo":"Buy groceries for the week","completed":false,"userId":2}],"total":200,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: limit must be at most 100.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/todos","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"limit","message":"limit must be at most 100"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is temporarily unavailable (`SERVICE_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"todosCreate","summary":"Create a todo (simulated)","description":"Validates the body and returns the todo as it would be created, with the next available ID. Nothing is persisted: the shared dataset is never modified.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Todos"],"parameters":[],"responses":{"201":{"description":"The simulated todo, including its new `id`.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":201,"todo":"Write integration tests","completed":false,"userId":5}}}},"400":{"description":"The request body is not valid JSON (`INVALID_JSON`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: todo: todo is required; userId: userId is required","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/todos","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"todo","message":"todo is required"},{"path":"userId","message":"userId is required"}]}}}}},"requestBody":{"required":true,"description":"Todos fields.","content":{"application/json":{"schema":{"type":"object","properties":{"todo":{"type":"string","minLength":1,"maxLength":300},"completed":{"type":"boolean"},"userId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000}},"required":["todo","userId"],"additionalProperties":false},"example":{"todo":"Write integration tests","completed":false,"userId":5}}}}}},"/api/v1/todos/{id}":{"get":{"operationId":"todosGet","summary":"Get a todo","description":"Returns a single todo by its numeric ID.","tags":["Todos"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric todo ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, todo, completed, userId.","schema":{"type":"string"},"example":"id,todo,completed"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"The todo object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Todo"},"example":{"id":1,"todo":"Reply to project kickoff email","completed":false,"userId":1}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Todo with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/todos/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}},"put":{"operationId":"todosReplace","summary":"Replace a todo (simulated)","description":"Validates a complete replacement body and returns the resulting todo. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Todos"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric todo ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The replaced todo.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"todo":"Review pull request","completed":true,"userId":1}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Todo with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/todos/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"description":"Complete todo representation.","content":{"application/json":{"schema":{"type":"object","properties":{"todo":{"type":"string","minLength":1,"maxLength":300},"completed":{"type":"boolean"},"userId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000}},"required":["todo","userId"],"additionalProperties":false},"example":{"todo":"Review pull request","completed":true,"userId":1}}}}},"patch":{"operationId":"todosPatch","summary":"Update a todo (simulated)","description":"Merges the provided fields into the existing todo and returns the result. The stored record is not changed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Todos"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric todo ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"The updated todo.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":1,"todo":"Reply to project kickoff email","completed":true,"userId":1}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Validation failed: unknownField: Unknown field(s): unknownField","status":422,"code":"VALIDATION_ERROR","path":"/api/v1/todos/1","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"unknownField","message":"Unknown field(s): unknownField"}]}}}}},"requestBody":{"required":true,"description":"Any subset of todo fields.","content":{"application/json":{"schema":{"type":"object","properties":{"todo":{"type":"string","minLength":1,"maxLength":300},"completed":{"type":"boolean"},"userId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":1000000000}},"additionalProperties":false},"example":{"completed":true}}}}},"delete":{"operationId":"todosDelete","summary":"Delete a todo (simulated)","description":"Returns `204 No Content` if the todo exists. The record is not actually removed.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Todos"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric todo ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"204":{"description":"Empty body."},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Todo with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/todos/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/carts":{"get":{"operationId":"cartsList","summary":"List carts","description":"Returns a paginated list of carts. Supports pagination, field selection, sorting, and filtering.","tags":["Carts"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, userId, products, total, discountedTotal, totalProducts, totalQuantity.","schema":{"type":"string"},"example":"id,userId,products"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","userId","total","discountedTotal","totalQuantity"],"default":"id"},"example":"userId"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"userId","in":"query","required":false,"description":"Only return carts that belong to this user ID.","schema":{"type":"integer"},"example":5},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of carts with `total`, `skip`, and `limit` metadata.","content":{"application/json":{"schema":{"type":"object","required":["carts","total","skip","limit"],"properties":{"carts":{"type":"array","items":{"$ref":"#/components/schemas/Cart"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"carts":[{"id":1,"userId":1,"products":[{"id":100,"title":"Aquaform Kickboard","price":117.99,"quantity":2,"total":235.98,"discountPercentage":0.09,"discountedTotal":235.77,"thumbnail":"http://localhost:4000/api/v1/images/products/100"},{"id":7,"title":"Orbitel One Smartphone","price":1192.99,"quantity":2,"total":2385.98,"discountPercentage":13.24,"discountedTotal":2070.08,"thumbnail":"http://localhost:4000/api/v1/images/products/7"},{"id":49,"title":"Lumio Watch Sport Strap","price":546.99,"quantity":4,"total":2187.96,"discountPercentage":1.02,"discountedTotal":2165.64,"thumbnail":"http://localhost:4000/api/v1/images/products/49"}],"total":4809.92,"discountedTotal":4471.49,"totalProducts":3,"totalQuantity":8},{"id":2,"userId":75,"products":[{"id":85,"title":"Dewdrop Gentle Foaming Cleanser","price":51.99,"quantity":1,"total":51.99,"discountPercentage":3.93,"discountedTotal":49.95,"thumbnail":"http://localhost:4000/api/v1/images/products/85"},{"id":16,"title":"Arcbook Go 12 Chromebook","price":1982.99,"quantity":3,"total":5948.97,"discountPercentage":18.14,"discountedTotal":4869.83,"thumbnail":"http://localhost:4000/api/v1/images/products/16"}],"total":6000.96,"discountedTotal":4919.78,"totalProducts":2,"totalQuantity":4}],"total":50,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: limit must be at most 100.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/carts","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"limit","message":"limit must be at most 100"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is temporarily unavailable (`SERVICE_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/carts/{id}":{"get":{"operationId":"cartsGet","summary":"Get a cart","description":"Returns a single cart by its numeric ID.","tags":["Carts"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric cart ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, userId, products, total, discountedTotal, totalProducts, totalQuantity.","schema":{"type":"string"},"example":"id,userId,products"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"The cart object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Cart"},"example":{"id":1,"userId":1,"products":[{"id":100,"title":"Aquaform Kickboard","price":117.99,"quantity":2,"total":235.98,"discountPercentage":0.09,"discountedTotal":235.77,"thumbnail":"http://localhost:4000/api/v1/images/products/100"},{"id":7,"title":"Orbitel One Smartphone","price":1192.99,"quantity":2,"total":2385.98,"discountPercentage":13.24,"discountedTotal":2070.08,"thumbnail":"http://localhost:4000/api/v1/images/products/7"},{"id":49,"title":"Lumio Watch Sport Strap","price":546.99,"quantity":4,"total":2187.96,"discountPercentage":1.02,"discountedTotal":2165.64,"thumbnail":"http://localhost:4000/api/v1/images/products/49"}],"total":4809.92,"discountedTotal":4471.49,"totalProducts":3,"totalQuantity":8}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Cart with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/carts/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/recipes":{"get":{"operationId":"recipesList","summary":"List recipes","description":"Returns a paginated list of recipes. Supports pagination, field selection, sorting, and filtering.","tags":["Recipes"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, name, cuisine, difficulty, mealType, tags, ingredients, instructions, prepTimeMinutes, cookTimeMinutes, servings, caloriesPerServing, rating, reviewCount, userId, image.","schema":{"type":"string"},"example":"id,name,cuisine"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","name","rating","prepTimeMinutes","cookTimeMinutes","caloriesPerServing"],"default":"id"},"example":"name"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"cuisine","in":"query","required":false,"description":"Filter by cuisine.","schema":{"type":"string"},"example":"Italian"},{"name":"difficulty","in":"query","required":false,"description":"Filter by difficulty.","schema":{"type":"string","enum":["Easy","Medium","Hard"]},"example":"Easy"},{"name":"mealType","in":"query","required":false,"description":"Filter by meal type.","schema":{"type":"string","enum":["Breakfast","Lunch","Dinner","Snack","Dessert"]},"example":"Dinner"},{"name":"tag","in":"query","required":false,"description":"Only return recipes with this tag.","schema":{"type":"string"},"example":"vegetarian"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of recipes with `total`, `skip`, and `limit` metadata.","content":{"application/json":{"schema":{"type":"object","required":["recipes","total","skip","limit"],"properties":{"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"recipes":[{"id":1,"name":"Classic Margherita Pizza","cuisine":"Italian","difficulty":"Medium","mealType":["Dinner"],"tags":["pizza","vegetarian"],"ingredients":["pizza dough","tomato sauce","fresh mozzarella","fresh basil","olive oil","salt"],"instructions":["Preheat the oven to its highest setting with a tray or pizza stone inside.","Stretch the pizza dough into a thin round on a floured surface.","Spread a thin layer of tomato sauce and tear the mozzarella over the top.","Bake for 8-10 minutes until the crust is blistered and the cheese is bubbling.","Finish with fresh basil and a drizzle of olive oil."],"prepTimeMinutes":18,"cookTimeMinutes":11,"servings":2,"caloriesPerServing":845,"rating":4.9,"reviewCount":15,"userId":87,"image":"http://localhost:4000/api/v1/images/recipes/1"},{"id":2,"name":"Spaghetti Aglio e Olio","cuisine":"Italian","difficulty":"Easy","mealType":["Dinner"],"tags":["pasta","vegetarian","quick"],"ingredients":["spaghetti","garlic","olive oil","red pepper flakes","parsley","parmesan"],"instructions":["Bring a large pot of salted water to a boil.","Cook the spaghetti until al dente, then reserve a cup of the cooking water.","Meanwhile, gently cook the remaining ingredients in olive oil over medium heat.","Toss the drained spaghetti with the sauce, adding cooking water as needed.","Serve immediately, topped with parmesan."],"prepTimeMinutes":11,"cookTimeMinutes":11,"servings":2,"caloriesPerServing":553,"rating":4.6,"reviewCount":109,"userId":39,"image":"http://localhost:4000/api/v1/images/recipes/2"}],"total":50,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: limit must be at most 100.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/recipes","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"limit","message":"limit must be at most 100"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is temporarily unavailable (`SERVICE_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/recipes/search":{"get":{"operationId":"recipesSearch","summary":"Search recipes","description":"Case-insensitive search across name, cuisine, tags, ingredients. Returns an empty `recipes` array with `total: 0` when nothing matches.","tags":["Recipes"],"parameters":[{"name":"q","in":"query","required":true,"description":"Search term (1-100 characters).","schema":{"type":"string"},"example":"pasta"},{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, name, cuisine, difficulty, mealType, tags, ingredients, instructions, prepTimeMinutes, cookTimeMinutes, servings, caloriesPerServing, rating, reviewCount, userId, image.","schema":{"type":"string"},"example":"id,name,cuisine"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","name","rating","prepTimeMinutes","cookTimeMinutes","caloriesPerServing"],"default":"id"},"example":"name"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"Matching recipes (possibly empty).","content":{"application/json":{"schema":{"type":"object","required":["recipes","total","skip","limit"],"properties":{"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"recipes":[{"id":2,"name":"Spaghetti Aglio e Olio","cuisine":"Italian","difficulty":"Easy","mealType":["Dinner"],"tags":["pasta","vegetarian","quick"],"ingredients":["spaghetti","garlic","olive oil","red pepper flakes","parsley","parmesan"],"instructions":["Bring a large pot of salted water to a boil.","Cook the spaghetti until al dente, then reserve a cup of the cooking water.","Meanwhile, gently cook the remaining ingredients in olive oil over medium heat.","Toss the drained spaghetti with the sauce, adding cooking water as needed.","Serve immediately, topped with parmesan."],"prepTimeMinutes":11,"cookTimeMinutes":11,"servings":2,"caloriesPerServing":553,"rating":4.6,"reviewCount":109,"userId":39,"image":"http://localhost:4000/api/v1/images/recipes/2"},{"id":3,"name":"Creamy Mushroom Pasta","cuisine":"Italian","difficulty":"Easy","mealType":["Lunch","Dinner"],"tags":["pasta","vegetarian"],"ingredients":["penne","mushrooms","cream","garlic","parmesan","fresh thyme"],"instructions":["Bring a large pot of salted water to a boil.","Cook the penne until al dente, then reserve a cup of the cooking water.","Meanwhile, gently cook the remaining ingredients in olive oil over medium heat.","Toss the drained penne with the sauce, adding cooking water as needed.","Serve immediately, topped with fresh thyme."],"prepTimeMinutes":17,"cookTimeMinutes":22,"servings":4,"caloriesPerServing":470,"rating":4.9,"reviewCount":101,"userId":72,"image":"http://localhost:4000/api/v1/images/recipes/3"}],"total":2,"skip":0,"limit":2}}}},"400":{"description":"Missing or invalid `q` parameter (`INVALID_PARAMETER`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: q is required.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/recipes/search","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"q","message":"q is required"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/recipes/{id}":{"get":{"operationId":"recipesGet","summary":"Get a recipe","description":"Returns a single recipe by its numeric ID.","tags":["Recipes"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric recipe ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, name, cuisine, difficulty, mealType, tags, ingredients, instructions, prepTimeMinutes, cookTimeMinutes, servings, caloriesPerServing, rating, reviewCount, userId, image.","schema":{"type":"string"},"example":"id,name,cuisine"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"The recipe object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"},"example":{"id":1,"name":"Classic Margherita Pizza","cuisine":"Italian","difficulty":"Medium","mealType":["Dinner"],"tags":["pizza","vegetarian"],"ingredients":["pizza dough","tomato sauce","fresh mozzarella","fresh basil","olive oil","salt"],"instructions":["Preheat the oven to its highest setting with a tray or pizza stone inside.","Stretch the pizza dough into a thin round on a floured surface.","Spread a thin layer of tomato sauce and tear the mozzarella over the top.","Bake for 8-10 minutes until the crust is blistered and the cheese is bubbling.","Finish with fresh basil and a drizzle of olive oil."],"prepTimeMinutes":18,"cookTimeMinutes":11,"servings":2,"caloriesPerServing":845,"rating":4.9,"reviewCount":15,"userId":87,"image":"http://localhost:4000/api/v1/images/recipes/1"}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Recipe with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/recipes/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/quotes":{"get":{"operationId":"quotesList","summary":"List quotes","description":"Returns a paginated list of quotes. Supports pagination, field selection, sorting, and filtering.","tags":["Quotes"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Number of records to return (1-100).","schema":{"type":"integer","default":30,"minimum":1,"maximum":100},"example":10},{"name":"skip","in":"query","required":false,"description":"Number of records to skip before returning results.","schema":{"type":"integer","default":0,"minimum":0,"maximum":1000000},"example":0},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, quote, author.","schema":{"type":"string"},"example":"id,quote,author"},{"name":"sortBy","in":"query","required":false,"description":"Field to sort by.","schema":{"type":"string","enum":["id","author"],"default":"id"},"example":"author"},{"name":"order","in":"query","required":false,"description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"example":"desc"},{"name":"author","in":"query","required":false,"description":"Filter by author (case-insensitive exact match).","schema":{"type":"string"},"example":"Mara Ellison"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A page of quotes with `total`, `skip`, and `limit` metadata.","content":{"application/json":{"schema":{"type":"object","required":["quotes","total","skip","limit"],"properties":{"quotes":{"type":"array","items":{"$ref":"#/components/schemas/Quote"}},"total":{"type":"integer","description":"Total number of matching records."},"skip":{"type":"integer"},"limit":{"type":"integer"}}},"example":{"quotes":[{"id":1,"quote":"Small steps taken daily outrun big plans made yearly.","author":"Mara Ellison"},{"id":2,"quote":"A good question is a map; a good answer is only a destination.","author":"Tobias Wren"}],"total":50,"skip":0,"limit":2}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: limit must be at most 100.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/quotes","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"limit","message":"limit must be at most 100"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is temporarily unavailable (`SERVICE_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/quotes/random":{"get":{"operationId":"quotesRandom","summary":"Get a random quote","description":"Returns one randomly selected quote. This is the only intentionally non-deterministic endpoint.","tags":["Quotes"],"parameters":[{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"A quote object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"},"example":{"id":28,"quote":"Teach what you learn and you will learn it twice.","author":"Lena Okoro"}}}}}}},"/api/v1/quotes/{id}":{"get":{"operationId":"quotesGet","summary":"Get a quote","description":"Returns a single quote by its numeric ID.","tags":["Quotes"],"parameters":[{"name":"id","in":"path","required":true,"description":"Numeric quote ID (positive integer).","schema":{"type":"integer","minimum":1},"example":1},{"name":"select","in":"query","required":false,"description":"Comma-separated list of fields to return. `id` is always included. Allowed: id, quote, author.","schema":{"type":"string"},"example":"id,quote,author"},{"name":"delay","in":"query","required":false,"description":"Delay the response by this many milliseconds (0-5000). Subject to a stricter rate limit.","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"The quote object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"},"example":{"id":1,"quote":"Small steps taken daily outrun big plans made yearly.","author":"Mara Ellison"}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Quote with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/quotes/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/auth/login":{"post":{"operationId":"authLogin","summary":"Log in (demo)","description":"Exchanges demo credentials for a short-lived demo access token and a refresh token. Use `username` or `email`. `expiresInMins` (1-1440) lets you practice token expiry.","tags":["Authentication"],"parameters":[],"responses":{"200":{"description":"Tokens and the authenticated user.","content":{"application/json":{"schema":{"type":"object","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"tokenType":{"type":"string","enum":["Bearer"]},"expiresIn":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"demo":{"type":"boolean","enum":[true]},"notice":{"type":"string"},"user":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"role":{"type":"string","enum":["admin","moderator","user"]},"image":{"nullable":true,"type":"string"}},"required":["id","username","email","firstName","lastName","role","image"],"additionalProperties":false}},"required":["accessToken","refreshToken","tokenType","expiresIn","demo","notice","user"],"additionalProperties":false},"example":{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.demo-payload.demo-signature","refreshToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.demo-payload.demo-signature","tokenType":"Bearer","expiresIn":1800,"demo":true,"notice":"Demo token for practice only. Do not use Mock APIs authentication to secure real applications.","user":{"id":1,"username":"emilyc","email":"emily.carter@example.com","firstName":"Emily","lastName":"Carter","role":"admin","image":"http://localhost:4000/api/v1/images/users/1"}}}}},"401":{"description":"Wrong username/email or password (`INVALID_CREDENTIALS`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid username/email or password","status":401,"code":"INVALID_CREDENTIALS","path":"/api/v1/auth/login","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"description":"Demo credentials.","content":{"application/json":{"schema":{"type":"object","properties":{"username":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"password":{"type":"string","minLength":1,"maxLength":128},"expiresInMins":{"type":"integer","minimum":1,"maximum":1440}},"required":["password"],"additionalProperties":false},"example":{"username":"emilyc","password":"emilycpass","expiresInMins":30}}}}}},"/api/v1/auth/register":{"post":{"operationId":"authRegister","summary":"Register (demo, simulated)","description":"Validates a registration request and returns tokens for a simulated new user. The account is not stored, but the returned access token works with `/auth/me` until it expires.\n\n**Simulated:** this operation never modifies the shared dataset. Responses include the `X-MockAPIs-Simulated: true` header.","tags":["Authentication"],"parameters":[],"responses":{"201":{"description":"Tokens and the simulated user.","content":{"application/json":{"schema":{"type":"object","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"tokenType":{"type":"string","enum":["Bearer"]},"expiresIn":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"demo":{"type":"boolean","enum":[true]},"notice":{"type":"string"},"user":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"role":{"type":"string","enum":["admin","moderator","user"]},"image":{"nullable":true,"type":"string"}},"required":["id","username","email","firstName","lastName","role","image"],"additionalProperties":false}},"required":["accessToken","refreshToken","tokenType","expiresIn","demo","notice","user"],"additionalProperties":false},"example":{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.demo-payload.demo-signature","refreshToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.demo-payload.demo-signature","tokenType":"Bearer","expiresIn":1800,"demo":true,"notice":"Demo token for practice only. Do not use Mock APIs authentication to secure real applications.","user":{"id":101,"username":"jordanlee","email":"jordan.lee@example.com","firstName":"Jordan","lastName":"Lee","role":"user","image":null}}}}},"409":{"description":"Username or email already taken by a seeded user (`CONFLICT`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"A user with this email or username already exists","status":409,"code":"CONFLICT","path":"/api/v1/auth/register","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"username","message":"username \"emilyc\" is already used by another user"}]}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"description":"New account details.","content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string","minLength":1,"maxLength":50},"lastName":{"type":"string","minLength":1,"maxLength":50},"username":{"type":"string","pattern":"^[a-zA-Z0-9._-]{3,30}$"},"email":{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"password":{"type":"string","minLength":8,"maxLength":72}},"required":["firstName","lastName","username","email","password"],"additionalProperties":false},"example":{"firstName":"Jordan","lastName":"Lee","username":"jordanlee","email":"jordan.lee@example.com","password":"supersecret1"}}}}}},"/api/v1/auth/me":{"get":{"operationId":"authMe","summary":"Get the current user","description":"Returns the user identified by the demo access token in the `Authorization: Bearer <token>` header.","tags":["Authentication"],"parameters":[],"responses":{"200":{"description":"The authenticated user.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"role":{"type":"string","enum":["admin","moderator","user"]},"image":{"nullable":true,"type":"string"}},"required":["id","username","email","firstName","lastName","role","image"],"additionalProperties":false},"example":{"id":1,"username":"emilyc","email":"emily.carter@example.com","firstName":"Emily","lastName":"Carter","role":"admin","image":"http://localhost:4000/api/v1/images/users/1"}}}},"401":{"description":"Missing header (`MISSING_TOKEN`), malformed header (`INVALID_AUTH_HEADER`), invalid token (`INVALID_TOKEN`), or expired token (`TOKEN_EXPIRED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"The access token is invalid.","status":401,"code":"INVALID_TOKEN","path":"/api/v1/auth/me","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}},"security":[{"demoBearer":[]}]}},"/api/v1/auth/refresh":{"post":{"operationId":"authRefresh","summary":"Refresh tokens","description":"Exchanges a valid refresh token for a new access token and refresh token.","tags":["Authentication"],"parameters":[],"responses":{"200":{"description":"New tokens and the user.","content":{"application/json":{"schema":{"type":"object","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"tokenType":{"type":"string","enum":["Bearer"]},"expiresIn":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"demo":{"type":"boolean","enum":[true]},"notice":{"type":"string"},"user":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"role":{"type":"string","enum":["admin","moderator","user"]},"image":{"nullable":true,"type":"string"}},"required":["id","username","email","firstName","lastName","role","image"],"additionalProperties":false}},"required":["accessToken","refreshToken","tokenType","expiresIn","demo","notice","user"],"additionalProperties":false},"example":{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.demo-payload.demo-signature","refreshToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.demo-payload.demo-signature","tokenType":"Bearer","expiresIn":1800,"demo":true,"notice":"Demo token for practice only. Do not use Mock APIs authentication to secure real applications.","user":{"id":1,"username":"emilyc","email":"emily.carter@example.com","firstName":"Emily","lastName":"Carter","role":"admin","image":"http://localhost:4000/api/v1/images/users/1"}}}}},"401":{"description":"Invalid or expired refresh token (`INVALID_TOKEN` / `TOKEN_EXPIRED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"The refresh token is invalid.","status":401,"code":"INVALID_TOKEN","path":"/api/v1/auth/refresh","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}},"422":{"description":"The JSON body failed validation (`VALIDATION_ERROR`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"description":"The refresh token from login.","content":{"application/json":{"schema":{"type":"object","properties":{"refreshToken":{"type":"string","minLength":1,"maxLength":4096},"expiresInMins":{"type":"integer","minimum":1,"maximum":1440}},"required":["refreshToken"],"additionalProperties":false},"example":{"refreshToken":"{{refreshToken}}","expiresInMins":30}}}}}},"/api/v1/auth/admin":{"get":{"operationId":"authAdmin","summary":"Admin-only route","description":"Practice role-based authorization. Returns 200 for users with the `admin` role and 403 for everyone else. Log in as `michaelb` to see a 403.","tags":["Authentication"],"parameters":[],"responses":{"200":{"description":"Access granted.","content":{"application/json":{"schema":{"type":"object"},"example":{"message":"Welcome, Emily! You have access to this admin-only demo route.","user":{"id":1,"username":"emilyc","email":"emily.carter@example.com","firstName":"Emily","lastName":"Carter","role":"admin","image":"http://localhost:4000/api/v1/images/users/1"}}}}},"401":{"description":"Missing or invalid token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Missing Authorization header. Send: Authorization: Bearer <accessToken>","status":401,"code":"MISSING_TOKEN","path":"/api/v1/auth/admin","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}},"403":{"description":"Authenticated, but not an admin (`FORBIDDEN`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"demoBearer":[]}]}},"/api/v1/test/echo":{"post":{"operationId":"testingEcho","summary":"Echo the request","description":"Returns the method, path, query parameters, safe request headers, and JSON body you sent. Any query parameters are accepted. The body is optional and must be a JSON object or array. Authorization headers, cookies, API keys, and sensitive body fields are redacted.","tags":["HTTP Testing"],"parameters":[],"responses":{"200":{"description":"The echoed request.","content":{"application/json":{"schema":{"type":"object"},"example":{"method":"POST","path":"/api/v1/test/echo","query":{"source":"docs"},"headers":{"host":"localhost:4000","user-agent":"curl/8.7.1","accept":"*/*","content-type":"application/json","x-custom-header":"demo","accept-encoding":"gzip, deflate","content-length":"<length>","connection":"close"},"body":{"message":"Hello, Mock APIs!","count":3},"contentType":"application/json","redacted":[]}}}},"400":{"description":"The request body is not valid JSON (`INVALID_JSON`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Request body contains invalid JSON","status":400,"code":"INVALID_JSON","path":"/api/v1/test/echo","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}},"413":{"description":"Body larger than the size limit (`PAYLOAD_TOO_LARGE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"description":"Any JSON object or array.","content":{"application/json":{"schema":{},"example":{"message":"Hello, Mock APIs!","count":3}}}}}},"/api/v1/test/delay":{"get":{"operationId":"testingDelay","summary":"Delayed response","description":"Waits `ms` milliseconds (0-5000) before responding. Great for testing loading states and client timeouts.","tags":["HTTP Testing"],"parameters":[{"name":"ms","in":"query","required":true,"description":"Delay in milliseconds (0-5000).","schema":{"type":"integer","minimum":0,"maximum":5000},"example":1000}],"responses":{"200":{"description":"Sent after the delay.","content":{"application/json":{"schema":{"type":"object","properties":{"delayedMs":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"message":{"type":"string"}},"required":["delayedMs","message"],"additionalProperties":false},"example":{"delayedMs":500,"message":"Responded after 500ms"}}}},"400":{"description":"Missing or out-of-range `ms` (`INVALID_PARAMETER`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Invalid query parameters: ms must be at most 5000.","status":400,"code":"INVALID_PARAMETER","path":"/api/v1/test/delay","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73","errors":[{"path":"ms","message":"ms must be at most 5000"}]}}}},"429":{"description":"Too many requests from this IP (`RATE_LIMIT_EXCEEDED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/test/status/{code}":{"get":{"operationId":"testingStatus","summary":"Respond with a status code","description":"Responds with the requested HTTP status code. Allowed codes: 200, 201, 202, 204, 400, 401, 403, 404, 405, 409, 410, 418, 422, 429, 500, 501, 502, 503, 504. Error codes use the standard error body with code `SIMULATED_STATUS`; 204 has an empty body.","tags":["HTTP Testing"],"parameters":[{"name":"code","in":"path","required":true,"description":"One of the allowed status codes.","schema":{"type":"integer","enum":[200,201,202,204,400,401,403,404,405,409,410,418,422,429,500,501,502,503,504]},"example":404}],"responses":{"200":{"description":"Simulated success.","content":{"application/json":{"schema":{"type":"object"},"example":{"status":200,"message":"Simulated 200 OK response"}}}},"400":{"description":"The code is not in the allowed list (`INVALID_PARAMETER`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Simulated error (any allowed 4xx/5xx code).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"Simulated 404 Not Found response","status":404,"code":"SIMULATED_STATUS","path":"/api/v1/test/status/404","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/api/v1/images/{type}/{id}":{"get":{"operationId":"imagesGet","summary":"Placeholder image (SVG)","description":"Deterministic SVG placeholder used by the `image` and `thumbnail` fields of users, products, and recipes.","tags":["System"],"parameters":[{"name":"type","in":"path","required":true,"description":"Image type.","schema":{"type":"string","enum":["users","products","recipes"]},"example":"users"},{"name":"id","in":"path","required":true,"description":"Record ID.","schema":{"type":"integer","minimum":1},"example":1}],"responses":{"200":{"description":"An SVG image.","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"400":{"description":"Invalid path or query parameter (`INVALID_PARAMETER` / `INVALID_ID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No record exists with that ID (`RESOURCE_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"message":"No image: users record with id 999999 not found","status":404,"code":"RESOURCE_NOT_FOUND","path":"/api/v1/images/users/999999","requestId":"6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"}}}}}}},"/health":{"get":{"operationId":"systemHealth","summary":"Health check (liveness)","description":"Always returns 200 while the process is running, with the current database connection state.","tags":["System"],"parameters":[],"responses":{"200":{"description":"The service is running.","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"ok","version":"1.0.0","uptimeSeconds":42,"startedAt":"2026-01-01T00:00:00.000Z","database":"connected"}}}}}}},"/ready":{"get":{"operationId":"systemReady","summary":"Readiness check","description":"Returns 200 when the database is connected and seeded, otherwise 503.","tags":["System"],"parameters":[],"responses":{"200":{"description":"Ready to serve traffic.","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"ready","database":"connected"}}}},"503":{"description":"Not ready (`SERVICE_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"operationId":"systemOpenapi","summary":"OpenAPI specification","description":"The OpenAPI 3.0 document for this API. Import it into Postman, Insomnia, or code generators.","tags":["System"],"parameters":[],"responses":{"200":{"description":"OpenAPI JSON document.","content":{"application/json":{"schema":{"type":"object"},"example":{"note":"The full OpenAPI document is returned. Download it from /openapi.json."}}}}}}}},"components":{"schemas":{"Error":{"type":"object","properties":{"message":{"type":"string"},"status":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"code":{"type":"string"},"path":{"type":"string"},"requestId":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["message","status","code","path","requestId"],"additionalProperties":false},"User":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"firstName":{"type":"string"},"lastName":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"age":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"gender":{"type":"string","enum":["female","male","non-binary"]},"birthDate":{"type":"string"},"image":{"type":"string"},"role":{"type":"string","enum":["admin","moderator","user"]},"address":{"type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"postalCode":{"type":"string"},"country":{"type":"string"}},"required":["street","city","state","postalCode","country"],"additionalProperties":false},"company":{"type":"object","properties":{"name":{"type":"string"},"department":{"type":"string"},"title":{"type":"string"}},"required":["name","department","title"],"additionalProperties":false}},"required":["id","firstName","lastName","username","email","phone","age","gender","birthDate","image","role","address","company"],"additionalProperties":false},"Product":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"brand":{"type":"string"},"price":{"type":"number"},"discountPercentage":{"type":"number"},"rating":{"type":"number"},"stock":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"sku":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"availabilityStatus":{"type":"string","enum":["In Stock","Low Stock","Out of Stock"]},"thumbnail":{"type":"string"}},"required":["id","title","description","category","brand","price","discountPercentage","rating","stock","sku","tags","availabilityStatus","thumbnail"],"additionalProperties":false},"Post":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"title":{"type":"string"},"body":{"type":"string"},"userId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"tags":{"type":"array","items":{"type":"string"}},"reactions":{"type":"object","properties":{"likes":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"dislikes":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["likes","dislikes"],"additionalProperties":false},"views":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["id","title","body","userId","tags","reactions","views"],"additionalProperties":false},"Comment":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"body":{"type":"string"},"postId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"userId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"likes":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["id","body","postId","userId","likes"],"additionalProperties":false},"Todo":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"todo":{"type":"string"},"completed":{"type":"boolean"},"userId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["id","todo","completed","userId"],"additionalProperties":false},"Cart":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"userId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"products":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"title":{"type":"string"},"price":{"type":"number"},"quantity":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"total":{"type":"number"},"discountPercentage":{"type":"number"},"discountedTotal":{"type":"number"},"thumbnail":{"type":"string"}},"required":["id","title","price","quantity","total","discountPercentage","discountedTotal","thumbnail"],"additionalProperties":false}},"total":{"type":"number"},"discountedTotal":{"type":"number"},"totalProducts":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"totalQuantity":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["id","userId","products","total","discountedTotal","totalProducts","totalQuantity"],"additionalProperties":false},"Recipe":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"type":"string"},"cuisine":{"type":"string"},"difficulty":{"type":"string","enum":["Easy","Medium","Hard"]},"mealType":{"type":"array","items":{"type":"string","enum":["Breakfast","Lunch","Dinner","Snack","Dessert"]}},"tags":{"type":"array","items":{"type":"string"}},"ingredients":{"type":"array","items":{"type":"string"}},"instructions":{"type":"array","items":{"type":"string"}},"prepTimeMinutes":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"cookTimeMinutes":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"servings":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"caloriesPerServing":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"rating":{"type":"number"},"reviewCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"userId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"image":{"type":"string"}},"required":["id","name","cuisine","difficulty","mealType","tags","ingredients","instructions","prepTimeMinutes","cookTimeMinutes","servings","caloriesPerServing","rating","reviewCount","userId","image"],"additionalProperties":false},"Quote":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"quote":{"type":"string"},"author":{"type":"string"}},"required":["id","quote","author"],"additionalProperties":false}},"securitySchemes":{"demoBearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Demo access token from POST /api/v1/auth/login. For practice only."}}}}