hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses API documentation with OpenAPI and @hectoday/http

Why documentation

  • Undocumented APIs are unusable
  • The OpenAPI standard
  • Project setup

Describing your API

  • Paths and operations
  • Request parameters
  • Request bodies
  • Responses

Schemas and reuse

  • Component schemas
  • Zod to OpenAPI
  • Error schemas

Beyond endpoints

  • Authentication documentation
  • Tags and grouping
  • Examples and descriptions

Serving and consuming

  • Serving the spec
  • Interactive docs with Scalar
  • Generating client SDKs

Wrapping up

  • Versioned specs
  • Checklist

Responses

We have described what clients send to our API: parameters and request bodies. Now let’s describe what the API sends back. Every operation needs to document its responses. What status codes are possible? What does the body look like for each one?

This is the part of the spec that clients rely on most. When a frontend developer writes code to display a list of books, they need to know the exact shape of the response. When they write error handling, they need to know the exact shape of every error. Guessing does not work.

The response field in route config

In the project setup lesson, you saw that routes can define a response field:

route.get("/v2/books", {
  request: { query: BookQuerySchema },
  response: { 200: BookListSchema },
  resolve: (c) => {
    // ...
  },
});

The response field is a record that maps status codes to Zod schemas. When you pass these routes to @hectoday/openapi, it reads these schemas and generates the response section of the spec automatically. So every response schema you define in your route also becomes documentation.

Let’s look at what the generated spec looks like for different kinds of responses.

Success responses

Check the responses for the list books endpoint:

curl http://localhost:3000/openapi.json | jq '.paths["/v2/books"].get.responses'
{
  "200": {
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "data": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "id": { "type": "string", "format": "uuid" },
                  "title": { "type": "string" }
                  // ... other Book fields
                },
                "required": ["id", "title", "author", "genre", "ratings", "createdAt"],
                "additionalProperties": false
              }
            },
            "meta": {
              "type": "object",
              "properties": {
                "page": { "type": "integer" },
                "limit": { "type": "integer" },
                "total": { "type": "integer" }
              },
              "required": ["page", "limit", "total"],
              "additionalProperties": false
            }
          },
          "required": ["data", "meta"],
          "additionalProperties": false
        }
      }
    }
  }
}

The 200 response has a content section that describes the JSON body. The full schema is inlined directly in the response, showing the exact shape of the BookList object with its data array and meta pagination info.

In your route definition, this comes from:

response: {
  200: z.object({
    data: z.array(BookSchema),
    meta: z.object({
      page: z.number().int(),
      limit: z.number().int(),
      total: z.number().int(),
    }),
  }),
}

The @hectoday/openapi package converts this Zod schema into the OpenAPI response schema. You write Zod, the spec is generated.

POST responses

When a client creates a resource, the response uses a different status code:

route.post("/v2/books", {
  request: { body: CreateBookSchema },
  response: { 201: BookSchema },
  resolve: (c) => {
    // ...
  },
});

Check the generated spec:

curl http://localhost:3000/openapi.json | jq '.paths["/v2/books"].post.responses'
{
  "201": {
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "id": { "type": "string", "format": "uuid" },
            "title": { "type": "string" }
            // ... other Book fields
          },
          "required": ["id", "title", "author", "genre", "ratings", "createdAt"],
          "additionalProperties": false
        }
      }
    }
  }
}

A POST that creates a resource returns 201 (Created), not 200. The body contains the newly created Book object, with the full schema inlined.

Error responses

Here is where it gets really important. What happens when something goes wrong? You can define error responses in the route config too:

route.post("/v2/books", {
  request: { body: CreateBookSchema },
  response: {
    201: BookSchema,
    400: ValidationErrorSchema,
    401: ErrorSchema,
  },
  resolve: (c) => {
    // ...
  },
});

This generates response entries for all three status codes:

{
  "201": {
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "id": { "type": "string", "format": "uuid" },
            "title": { "type": "string" }
            // ... other Book fields
          },
          "required": ["id", "title", "author", "genre", "ratings", "createdAt"],
          "additionalProperties": false
        }
      }
    }
  },
  "400": {
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "error": {
              "type": "object",
              "properties": {
                "code": { "const": "VALIDATION_ERROR" },
                "message": { "type": "string" },
                "fields": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "array",
                    "items": { "type": "string" }
                  }
                }
              },
              "required": ["code", "message", "fields"],
              "additionalProperties": false
            }
          },
          "required": ["error"],
          "additionalProperties": false
        }
      }
    }
  },
  "401": {
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "error": {
              "type": "object",
              "properties": {
                "code": { "type": "string" },
                "message": { "type": "string" }
              },
              "required": ["code", "message"],
              "additionalProperties": false
            }
          },
          "required": ["error"],
          "additionalProperties": false
        }
      }
    }
  }
}

Now a client reading this spec knows exactly what to expect. If they send invalid data, they get a 400 with a ValidationError shape. If they forget their auth token, they get a 401 with an Error shape. They can write error handling code against these documented schemas with confidence.

What do you think happens if you only define the success response? Clients still get errors, but the spec does not describe them. The client has no idea what the error body looks like, so they end up with fragile error handling that breaks the moment you change anything. Documenting error responses is just as important as documenting success.

Status codes without bodies

Some status codes do not have a response body. A 204 (No Content) after deleting a resource is the classic example:

route.delete("/v2/books/:id", {
  response: {
    204: z.object({}),
    401: ErrorSchema,
    403: ErrorSchema,
    404: ErrorSchema,
  },
  resolve: (c) => {
    // ...
  },
});

The @hectoday/openapi package knows that 204, 205, and 304 responses cannot have bodies, so it omits the content section for those status codes automatically:

{
  "204": {
    "description": "No Content"
  },
  "401": {
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "error": {
              "type": "object",
              "properties": {
                "code": { "type": "string" },
                "message": { "type": "string" }
              },
              "required": ["code", "message"],
              "additionalProperties": false
            }
          },
          "required": ["error"],
          "additionalProperties": false
        }
      }
    }
  },
  "403": {
    "content": {
      "application/json": {
        "schema": {
          // ... same Error schema as 401
        }
      }
    }
  },
  "404": {
    "content": {
      "application/json": {
        "schema": {
          // ... same Error schema as 401
        }
      }
    }
  }
}

You will notice that the same Error schema is repeated for the 401, 403, and 404 responses. This is how @hectoday/openapi generates specs: each operation contains its full inlined schemas. We will discuss the implications of this duplication in the component schemas lesson.

Every possible status code should be documented. If a client can get a 403 from your endpoint, they need to see it in the spec. Otherwise they get an unexpected error with an unknown shape, and their app crashes instead of showing a helpful message.

That covers the four core pieces of an OpenAPI operation: paths, parameters, request bodies, and responses. In the next section, we will look at how OpenAPI handles schema duplication and what component schemas are for.

Exercises

Exercise 1: Add response schemas to a GET endpoint. Include the 200 success case with the full response shape. Restart the server and check the generated spec.

Exercise 2: Add response schemas for POST /v2/books with 201 (created), 400 (validation error), and 401 (unauthorized). Check the generated spec to see all three status codes.

Exercise 3: Add response schemas for DELETE /v2/books/:id with 204, 401, 403, and 404. Check the generated spec and notice which responses have bodies and which do not.

Why document error responses in addition to success responses?

← Request bodies Component schemas →

© 2026 hectoday. All rights reserved.