Adding OpenAPI and Swagger UI endpoints

One of the most popular ways to share your OpenAPI document with your users is to host it alongside your API server itself.

Adding-openapi-and-swagger-ui-endpoints.tutorial

Typically this is at /openapi.yaml or similar, which serves a plain-text OpenAPI document for consumption by clients.

Additionally, you can host an HTML page that renders the OpenAPI document as interactive documentation that you can use from the browser, for example using swagger-ui.

In this tutorial we’ll add both endpoints to our Vapor server.

Add an /openapi.yaml endpoint

We’ll start with the server in its state following Generating server stubs in a Swift package (before the /emoji endpoint is added) and we’ll add an /openapi.yaml endpoint that serves the OpenAPI document as a static resource.

  1. In the server package, create a Public/ directory for serving static content.

    console
    % mkdir Public
    server-openapi-endpoints.console.0.txt
  2. Move openapi.yaml from Sources/ and create a symlink to it back in the Sources/ directory.

    console
    % mkdir Public
    
    % mv Sources/openapi.yaml Public/
    
    % ln -s ../Public/openapi.yaml Sources/openapi.yaml
    server-openapi-endpoints.console.1.txt
  3. In main.swift, Add a Vapor middleware that serves the contents of the Public/ directory.

    main.swift
    import Foundation
    import Vapor
    import OpenAPIRuntime
    import OpenAPIVapor
    
    // Define a type that conforms to the generated protocol.
    struct GreetingServiceAPIImpl: APIProtocol {
        func getGreeting(
            _ input: Operations.getGreeting.Input
        ) async throws -> Operations.getGreeting.Output {
            let name = input.query.name ?? "Stranger"
            let greeting = Components.Schemas.Greeting(message: "Hello, \(name)!")
            return .ok(.init(body: .json(greeting)))
        }
    }
    
    // Create your Vapor application.
    let app = Vapor.Application()
    
    // Create a VaporTransport using your application.
    let transport = VaporTransport(routesBuilder: app)
    
    // Create an instance of your handler type that conforms the generated protocol
    // defining your service API.
    let handler = GreetingServiceAPIImpl()
    
    // Call the generated function on your implementation to add its request
    // handlers to the app.
    try handler.registerHandlers(on: transport, serverURL: Servers.Server1.url())
    
    // Add Vapor middleware to serve the contents of the Public/ directory.
    app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
    
    // Start the app as you would normally.
    try await app.execute()
    server-openapi-endpoints.main.1.swift
  4. From the Product menu, select Scheme > Edit Scheme.... With Run selected in the sidebar, select the Options tab and check the box labeled Use custom working directory and use the path containing Package.swift for this package.

    This step is necessary because the Vapor middleware serves files relative to the current working directory for the running server process.

  5. Test this endpoint in your browser, or using curl.

    console
    % mkdir Public
    
    % mv Sources/openapi.yaml Public/
    
    % ln -s ../Public/openapi.yaml Sources/openapi.yaml
    
    % curl "localhost:8080/openapi.yaml"
    openapi: '3.1.0'
    info:
      title: GreetingService
      version: 1.0.0
    servers:
      - url: https://example.com/api
        description: Example service deployment.
    paths:
      /greet:
        get:
          operationId: getGreeting
          parameters:
            - name: name
    ---[SNIP]---
    server-openapi-endpoints.console.2.txt

Add a Swagger UI endpoint

Now we’ll add a static openapi.html page that serves Swagger UI and add a redirect to this page from /openapi for discoverability.

  1. Create the file Public/openapi.html with the HTML contents as shown on the right.

    By placing it in the public directory, it is already reachable at /openapi.html.

    openapi.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@5/swagger-ui.css">
      <title>Swift OpenAPI Sample API</title>
    <body>
      <div id="openapi" />
      <script src="//unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
      <script>
        window.onload = function() {
          const ui = SwaggerUIBundle({
            url: "openapi.yaml",
            dom_id: "#openapi",
            deepLinking: true,
            validatorUrl: "none"
          })
        }
      </script>
    </body>
    server-openapi-endpoints.openapi.html
  2. Add a redirect for /openapi to openapi.html, which serves the rendered documentation.

    main.swift
    import Foundation
    import Vapor
    import OpenAPIRuntime
    import OpenAPIVapor
    
    // Define a type that conforms to the generated protocol.
    struct GreetingServiceAPIImpl: APIProtocol {
        func getGreeting(
            _ input: Operations.getGreeting.Input
        ) async throws -> Operations.getGreeting.Output {
            let name = input.query.name ?? "Stranger"
            let greeting = Components.Schemas.Greeting(message: "Hello, \(name)!")
            return .ok(.init(body: .json(greeting)))
        }
    }
    
    // Create your Vapor application.
    let app = Vapor.Application()
    
    // Create a VaporTransport using your application.
    let transport = VaporTransport(routesBuilder: app)
    
    // Create an instance of your handler type that conforms the generated protocol
    // defining your service API.
    let handler = GreetingServiceAPIImpl()
    
    // Call the generated function on your implementation to add its request
    // handlers to the app.
    try handler.registerHandlers(on: transport, serverURL: Servers.Server1.url())
    
    // Add Vapor middleware to serve the contents of the Public/ directory.
    app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
    
    // Redirect /openapi to openapi.html, which serves the rendered documentation.
    app.get("openapi") { $0.redirect(to: "/openapi.html", redirectType: .permanent) }
    
    // Start the app as you would normally.
    try await app.execute()
    server-openapi-endpoints.main.2.swift
  3. Add a relative server URL to the OpenAPI document.

    This allows you to use the rendered documentation to make requests to the instance serving the OpenAPI document itself.

    main.swift
    openapi: '3.1.0'
    info:
      title: GreetingService
      version: 1.0.0
    servers:
      - url: https://example.com/api
        description: Example service deployment.
      - url: /api
        description: The server hosting this document.
    paths:
      /greet:
        get:
          operationId: getGreeting
          parameters:
            - name: name
              required: false
              in: query
              description: The name used in the returned greeting.
              schema:
                type: string
          responses:
            '200':
              description: A success response with a greeting.
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/Greeting'
    components:
      schemas:
        Greeting:
          type: object
          properties:
            message:
              type: string
          required:
            - message
    server-openapi-endpoints.openapi.1.yaml
  4. Visit http://localhost:8080/openapi in your browser which should show the rendered documentation for the OpenAPI document.