Authentication documentation
Our spec now describes every endpoint’s parameters, request bodies, and responses. But there is a big piece missing. Most of our endpoints require authentication, and clients have no idea how to authenticate. What kind of token do they need? Where do they get it? How do they send it? Without this information in the spec, the first thing every client gets is a 401 error, and they have no idea what to do about it.
OpenAPI handles this with security schemes, and @hectoday/openapi lets you configure them in the openapi() config.
Defining security schemes
Security schemes are part of the config you pass to openapi(). Update src/app.ts:
const api = openapi(routes, {
info: { title: "Book Catalog API", version: "2.0.0" },
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
description:
"JWT token obtained from POST /v2/auth/login. " +
"Include in the Authorization header: Authorization: Bearer <token>",
},
},
security: [{ bearerAuth: [] }],
}); Let’s break this down. The securitySchemes object defines the authentication methods your API supports. Each key is a name (like bearerAuth) and the value describes the scheme. The type: "http" says this is standard HTTP authentication. The scheme: "bearer" says it uses Bearer tokens. The bearerFormat: "JWT" tells clients the token is a JSON Web Token. And the description explains where to get the token and how to include it.
The security array at the top level means: every endpoint in the API requires Bearer authentication by default. The empty array [] after bearerAuth means no specific scopes are required.
Restart the server and check what this produces:
curl http://localhost:3000/openapi.json | jq '{securitySchemes: .components.securitySchemes, security: .security}' {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT token obtained from POST /v2/auth/login. Include in the Authorization header: Authorization: Bearer <token>"
}
},
"security": [{ "bearerAuth": [] }]
} A client reading this spec knows everything they need: go to the login endpoint, get a JWT, put it in the Authorization header as Bearer <token>.
Common security scheme types
Different APIs use different authentication methods. Here are the most common ones:
Bearer token (JWT):
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
} API key in a header:
securitySchemes: {
apiKeyAuth: {
type: "apiKey",
name: "X-API-Key",
in: "header",
},
} API key in a query parameter:
securitySchemes: {
apiKeyQuery: {
type: "apiKey",
name: "api_key",
in: "query",
},
} OAuth2:
securitySchemes: {
oauth2: {
type: "oauth2",
flows: {
authorizationCode: {
authorizationUrl: "https://example.com/oauth/authorize",
tokenUrl: "https://example.com/oauth/token",
scopes: { read: "Read access", write: "Write access" },
},
},
},
} Our book catalog uses Bearer JWTs, so that is what we will use. But @hectoday/openapi supports all of these types, including OpenID Connect.
Global security
Setting security: [{ bearerAuth: [] }] in the config applies authentication globally. Every endpoint in the generated spec inherits this requirement. A client reading the spec knows: send a Bearer token with every request.
In the OpenAPI standard, individual endpoints can override global security by setting security: [] (an empty array) to mark them as public. @hectoday/openapi does not currently support per-route security overrides, so the global setting applies to all endpoints. If your API has public endpoints (like a login route), the spec will still show them as requiring authentication even though your server does not enforce it on those routes.
This is a documentation gap, not a runtime issue. Your server handles authentication in the route handlers or middleware regardless of what the spec says. The spec just tells clients what to expect.
Documenting auth-related responses
When an endpoint requires authentication, you should include the auth-related error responses in the route’s response schemas:
route.post("/v2/books", {
request: { body: CreateBookSchema },
response: {
201: BookSchema,
400: ValidationErrorSchema,
401: ErrorSchema,
403: ErrorSchema,
},
resolve: (c) => {
// ...
},
}); This tells clients: “You need a Bearer token. If you do not send one, you get 401. If your token is valid but you do not have the right permissions, you get 403.” No guessing.
Interactive docs with auth
When Scalar reads the security schemes in your spec, it shows an auth section where clients can enter their Bearer token. Once entered, all requests from the docs UI include the token automatically. Clients can test protected endpoints directly from the browser without switching to curl or Postman.
In the next lesson, we will organize our growing spec with tags so endpoints are grouped logically instead of dumped in a flat list.
Exercises
Exercise 1: Add securitySchemes and security to your openapi() config. Restart the server and check the generated spec to see them.
Exercise 2: Open the docs UI at /docs and look for the auth section. Enter a token and make an authenticated request.
Exercise 3: Add 401 and 403 response schemas to a protected endpoint. Check that they show up in the spec.
How does OpenAPI indicate that a specific endpoint is public (no auth required)?