Your API has many endpoints. Other developers need to know how to use them. They need to know the request format, response format, required headers, and possible error codes. Writing documentation by hand is tedious and quickly becomes outdated.

In this tutorial, you will add OpenAPI documentation and Swagger UI to your Ktor API. Developers can browse your API at /docs and try out endpoints directly from the browser.

What is OpenAPI?

OpenAPI (formerly Swagger) is a standard format for describing REST APIs. It is a YAML or JSON file that lists all endpoints, request bodies, response schemas, authentication, and more.

Swagger UI is a web interface that reads the OpenAPI file and displays an interactive API explorer.

Dependencies

dependencies {
    implementation("io.ktor:ktor-server-openapi:$ktorVersion")
    implementation("io.ktor:ktor-server-swagger:$ktorVersion")
}

Write the OpenAPI Spec

Create the file at src/main/resources/openapi/documentation.yaml:

openapi: 3.0.3
info:
  title: Ktor Tutorial API
  description: REST API built with Ktor
  version: 1.0.0

servers:
  - url: http://localhost:8080
    description: Local development

paths:
  /api/auth/register:
    post:
      tags: [Authentication]
      summary: Register a new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: User registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
        '400':
          description: Validation error
        '409':
          description: Email already exists

  /api/auth/login:
    post:
      tags: [Authentication]
      summary: Login and get tokens
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
        '401':
          description: Invalid credentials

  /api/notes:
    get:
      tags: [Notes]
      summary: List notes with filtering
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: size
          in: query
          schema: { type: integer, default: 10 }
        - name: tag
          in: query
          schema: { type: string }
      responses:
        '200':
          description: List of notes
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/NoteResponse'
    post:
      tags: [Notes]
      summary: Create a note
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateNoteRequest'
      responses:
        '201':
          description: Note created

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    RegisterRequest:
      type: object
      required: [name, email, password]
      properties:
        name: { type: string, example: Alex }
        email: { type: string, format: email }
        password: { type: string, minLength: 8 }

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email: { type: string }
        password: { type: string }

    TokenResponse:
      type: object
      properties:
        accessToken: { type: string }
        refreshToken: { type: string }
        expiresIn: { type: integer }

    NoteResponse:
      type: object
      properties:
        id: { type: integer }
        title: { type: string }
        content: { type: string }
        tags:
          type: array
          items: { type: string }

    CreateNoteRequest:
      type: object
      required: [title, content]
      properties:
        title: { type: string }
        content: { type: string }
        tags:
          type: array
          items: { type: string }

This is a simplified example. The full spec in the source code documents all endpoints.

Serve the Docs

Create a plugin to serve the OpenAPI spec and Swagger UI:

fun Application.configureOpenApi() {
    routing {
        // Serve OpenAPI spec at /openapi
        openAPI(path = "openapi", swaggerFile = "openapi/documentation.yaml")

        // Serve Swagger UI at /docs
        swaggerUI(path = "docs", swaggerFile = "openapi/documentation.yaml") {
            version = "5.17.14"
        }
    }
}

Install it in your application module:

fun Application.module() {
    // ... other plugins
    configureOpenApi()
    configureRouting()
}

Now visit http://localhost:8080/docs to see the interactive API documentation.

Documenting Endpoints

Tags

Group endpoints with tags:

paths:
  /api/auth/register:
    post:
      tags: [Authentication]

Security

Mark protected endpoints:

  /api/auth/me:
    get:
      security:
        - bearerAuth: []

Request/Response Examples

Add examples to make the docs more helpful:

RegisterRequest:
  type: object
  properties:
    name:
      type: string
      example: Alex
    email:
      type: string
      example: alex@example.com
    password:
      type: string
      example: password123

Error Responses

Document error responses:

responses:
  '400':
    description: Validation error
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/ErrorResponse'
  '401':
    description: Unauthorized
  '404':
    description: Not found

Generating Client SDKs

The OpenAPI spec can generate client code. Use openapi-generator:

# Generate TypeScript client
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:8080/openapi \
  -g typescript-axios \
  -o ./client

# Generate Kotlin client
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:8080/openapi \
  -g kotlin \
  -o ./kotlin-client

This generates a type-safe client that matches your API exactly.

Keeping Docs in Sync

The biggest challenge with OpenAPI is keeping the spec file in sync with your code. Some strategies:

  1. Write the spec first (API-first design) — then implement the endpoints to match
  2. Update the spec when you change code — add it to your code review checklist
  3. Use tests to validate — test that endpoints match the documented schemas

Common Mistakes

  1. Not documenting error responses — Developers need to know what errors to handle.
  2. Missing response schemas — Without schemas, the Swagger UI cannot show response examples.
  3. Outdated documentation — If you change an endpoint and forget to update the spec, developers will get confused.
  4. Not using tags — Without tags, all endpoints appear in one long list.

Source Code

You can find the source code for this tutorial on GitHub:

github.com/kemalcodes/ktor-tutorial — Branch: tutorial-16-openapi

What’s Next?

You have API documentation at /docs. In the next tutorial, you will add HTMX with Ktor — building a server-rendered admin dashboard with dynamic updates.