DEV Community

Cover image for Integrate SwaggerUI into Vue 3 — and extend it like a Jedi Dev
pascalboth
pascalboth

Posted on

Integrate SwaggerUI into Vue 3 — and extend it like a Jedi Dev

TL;DR: Learn how to integrate Swagger UI into a Vue 3 application and create custom plugins to extend its functionality. This guide walks you through setting up SwaggerUI, creating a plugin and syncing state between Vue and the plugin. It also covers how to type your plugin components and plugin state for better TypeScript integration.


A long time ago in a galaxy far, far away, a developer faced the challenge of integrating SwaggerUI into a Vue 3 application. The goal was to add a custom table with additional information to each API operation. The main challenge was synchronizing state between the Vue application and Swagger UI’s plugin system, which is built on React.

In this article, I'll show you how to integrate SwaggerUI into a Vue app, create custom plugins, and sync data by visualizing a fictional Star Wars API.

Integrate Swagger UI into your application

First you need to install the package from npm. There are 3 different npm modules you can choose from. The indidivual purpose of the packages are described here. I will use swagger-ui because it's the recommended package to use in Single Page Applications.

npm install swagger-ui
Enter fullscreen mode Exit fullscreen mode

Now that we have installed the package we can integrate it into our Vue application. SwaggerUI is initialized in the onMounted lifecycle hook to ensure the DOM is fully rendered before mounting SwaggerUI into the UI.

<template>
   <!-- DOM element used for rendering the Swagger UI into -->
   <div id="swagger-ui"></div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import SwaggerUI from 'swagger-ui';

onMounted(()=> {
   SwaggerUI({
       dom_id: '#swagger-ui',
   })
})
</script>
Enter fullscreen mode Exit fullscreen mode

You can now pass in an Open API specification file directly into the viewer via the spec property or reference it from a server url with the url property. For this example I will use a local Open API specification. All the other configuration options for SwaggerUI can be found in the official documentation here.

<template>
   <!-- DOM element used for rendering the Swagger UI into -->
   <div id="swagger-ui"></div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import SwaggerUI from 'swagger-ui';

const mySpecification = `
openapi: 3.0.0
info:
  title: "Minimal Star Wars API"
  version: 1.0.0
paths:
  /characters:
    get:
      summary: Returns all Star Wars characters
      responses:
        '200':
          description: "A successful response"
`

onMounted(()=> {
   SwaggerUI({
       dom_id: '#swagger-ui',
       spec: mySpecification, // Reference local open api specification
   })
})
</script>
Enter fullscreen mode Exit fullscreen mode

Here’s what the integrated SwaggerUI should look like right now:

SwaggerUI integrated

That's it! If you just want to integrate Swagger UI into your Vue application you're finished now. Integrated Swagger UI, you have. Extend it with plugins, next you must.


Creating your first plugin

SwaggerUI is highly customizable due to it's redux based architecture. It lets you overwrite existing state and their state operations like actions or selectors. Additionally you can also create new state, actions, selectors, reducers, and wrap existing UI components or create new ones to customize SwaggerUI to your needs.

SwaggerUI is built with React, so you also have to use React to create plugins for it. Luckily you don't need to add React as a dependency because the library provides you a React instance. It can be accessed in each plugin within the system parameter. The system parameter is passed into each Plugin and contains not just the React instance but also all pre defined state, selectors, actions and much more. You can easily find out what it contains by adding a log statement in your plugin.

We start simple with the first plugin. This plugin should show the number of endpoints we have described in our Open API specification. To do so, we're wrapping the info component. The info component represents the info section at the top of SwaggerUI containing the API title, description and other meta info like the server url.

export const InfoPlugin = function (system) {  
    const React = system.React // You always need to create this variable even if you don't use it, otherwise React context is missing. 

    return {  
        wrapComponents: {  
            info: (Original, system) => (props) => {  
                const nrOfEndpoints = useMemo(  
                    () =>  
                        Object.entries(system.specSelectors.operations().toJS()) // <-- Retrieving the data from the pre-built specSelectors 
                            .length,  
                    [system.specSelectors.operations().toJS()]  
                )

                return (  
                    <div>  
                        <Original {...props} />  
                        <p class="mb-4">  
                            This api contains <b>{nrOfEndpoints}</b> endpoints.  
                        </p>  
                    </div>  
                )  
            },  
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

The structure of a plugin is always defined like the following. You're creating a function which returns an object containing all the components you want to wrap in this plugin. Additionally you can define or wrap existing state within a plugin. I will cover this in the next chapter.

For wrapping a component we're creating a Higher Order React component. You always get the original component via the Original param. As a second parameter you get the system context. This function then returns another function in which you can access the props passed into the component.

Tip: There is unfortunately no list of all available components and their component names. Therefore you need to check out the swagger-ui repository to find the individual component name.

To show the number of endpoints in the info component we can use the existing specSelectors. These let you select the operations of the given Open API specification. To retrieve the data in a usable format we need to use the toJS function on the returned values. All state is immutable due to the usage of immutable-js. With toJS we can convert the immutable data into a JavaScript value.

All state related methods are composed by the name of the state, followed by the type of operation (e.g. selectors). For the spec state it's specSelectors and specActions.

Here’s what the SwaggerUI with the newly created plugin should look like right now:
InfoPlugin displaying the number of endpoints

Mastered the plugin, you have. Now, connect Vue and Swagger UI, you must. Sync the state, you will.


Synching state between a Vue component and a plugin

Now we can get to the more challenging part. Synchronizing the state between the Vue application and a plugin. We want to visualize some Star Wars characters to showcase what the API is capable of.
For this we're defining a new state called characters. The characters are stored in the Vue application and therefore we're creating an action to set the characters, which is then processed by the respective reducer. To select the characters from the store we're creating the getAllCharacters selector.

export const InfoPlugin = function (system) {  
    const React = system.React

    return {  
        wrapComponents: {  
            info: (Original, system) => (props) => { 
              // ...  
            },
        statePlugins: {  
            // New state we defined
            characters: {  
                selectors: {  
                    getAllCharacters: (state) => state.get('characters'),  
                },  
                actions: {  
                    setCharacters: (characters) => ({  
                        type: 'GET_STAR_WARS_CHARACTER',  
                        payload: characters,  
                    }),  
                },  
                reducers: {  
                    'GET_STAR_WARS_CHARACTER': (state, action) => {  
                        return state.set('characters', action.payload)  
                    },  
                },  
            },  
            },  
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that we have created a statePlugin we can pass the data from our vue component into SwaggerUI by dispatching a redux action. The dispatch method is returned by SwaggerUI instance as you can see in the code example below.

onMounted(() => {  
    const swaggerUi = SwaggerUI({  
        dom_id: '#swagger-ui',  
        spec: starWarsApi,  
        plugins: [InfoPlugin],  
    })  

    const store = swaggerUi.getSystem().getStore()  

    // Example API call
    fetch('https://swapi.dev/api/people')  
        .then((res) => res.json())  
        .then((data) => {  
            const action = {  
                type: 'GET_STAR_WARS_CHARACTER',  
                payload: toRaw(data.results), // Pass value without proxy object so that we can use it properly in React  
            }  

            store.dispatch(action) // dispatches the GET_STAR_WARS_CHARACTER action to sync data between vue component and plugin
        })  
        .catch((error) => console.error('Error fetching data:', error))  
})
Enter fullscreen mode Exit fullscreen mode

Now that the character data is passed to the characters state we can extend the info component by showing the character info. For each character we're rendering a CharacterCard. It's a reusable React component, which can be found in the repository here.

There is one thing to note when creating reusable React components in the context of SwaggerUI. If you don't have React as a dependency you need to pass the React instance from the plugin into the CharacterCard. Otherwise the component won't be rendered.

export const InfoPlugin = function (system: System) {  
    const React = system.React  
    const { useMemo } = React  
    return {  
        wrapComponents: {  
            info: (Original, system: System) => (props) => {  
                const nrOfEndpoints = useMemo(()=> /* ... */ )  

                // Select the characters from characters store
                const allCharacters = useMemo(  
                    () => system.charactersSelectors.getAllCharacters(),  
                    [system.charactersSelectors.getAllCharacters()]  
                )  

                return (  
                    <div>  
                        <Original {...props} />  
                        <p class="mb-4">  
                            This api contains <b>{nrOfEndpoints}</b> endpoints.  
                        </p>  

                        <h3 class="text-2xl mb-4">  
                            Showcase the capabilities of the Character API  
                            endpoints:  
                        </h3>  
                        <div class="flex flex-wrap gap-4">  
                            {allCharacters?.map((character) => (  
                                <div class="w-full sm:w-1/2 lg:w-1/3 xl:w-1/4">  
                                    <CharacterCard  
                                        key={character.name}  
                                        character={character}  
                                        React={React}  
                                    />  
                                </div>  
                            ))}  
                        </div>  
                    </div>  
                )  
            },  
        },  
        statePlugins: {  
            // ...
        },  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Synching reactive data

If you have a ref or computed value you want to pass to SwaggerUI you can easily do that by setting up a watch function and call the respective dispatch method as seen in the example below.

const characters = computed(()=> myImaginaryApiResponse.value)

watch(characters, (newVal)=> {
    const action = {  
        type: 'GET_STAR_WARS_CHARACTER',  
        payload: toRaw(data.results), // <-- Pass value without proxy object so that we can use it within the React based plugins
    }  

    store.dispatch(action)
})
Enter fullscreen mode Exit fullscreen mode

Here’s how the CharacterCards look like:
Info plugin visualizing the character info to showcase the capabilities of the API

Typed your code, you must. Ensure the Force of TypeScript strong in your Swagger UI plugins, it will.


Typing your plugins

When using SwaggerUI with TypeScript there are several things you should take care of in your typescript configuration.

The following settings should be set to true in the tsconfig.json

  • esModuleInterop
  • allowSyntheticDefaultImports

Since JSX uses className instead of class for assigning CSS classes, you’ll need to add the following configuration to your tsconfig.app.json file:

"jsx": "react-jsx",  
"jsxImportSource": "react",
Enter fullscreen mode Exit fullscreen mode

If you don't set these property your build will fail with an error like this:

src/plugins/InfoPlugin.tsx:55:30 - error TS2322: Type '{ className: string; }' is not assignable to type 'HTMLAttributes & ReservedProps'.  
Property 'className' does not exist on type 'HTMLAttributes & ReservedProps'.
Enter fullscreen mode Exit fullscreen mode

Typing SwaggerUI instance

For typesafety of the SwaggerUI instance, you need to create your own interface which extends the SwaggerUI instance. It could look like the following:

<script setup lang="ts">
interface SwaggerUiInstance extends SwaggerUI {
    getSystem: () => {
        getStore: () => {
            dispatch: (action: GetStarWarsCharactersAction) => void
        }
    }
}

onMounted(() => {
    const swaggerUi = SwaggerUI({
        dom_id: '#swagger-ui',
        spec: starWarsApi,
        plugins: [InfoPlugin],
    }) as SwaggerUiInstance
    // ...
}
</script>
Enter fullscreen mode Exit fullscreen mode

Apart from that you can do the following things to provide some kind of typesafety for your plugins and state operations. Here are some examples for different use cases.

Typing system param

For the info plugin I created an interface of all used properties and store operations like below:

interface System {  
    React: any  
    charactersSelectors: {  
        getAllCharacters: () => StarWarsCharacter[]  
    }  
    specSelectors: {  
        operations: () => { toJS: () => Record<string, unknown> }  
    }  
}

export const InfoPlugin = function (system: System) { ... }
Enter fullscreen mode Exit fullscreen mode

Typing state related operations

I'd recommend to create types for the actions and their action types. Additionally you can create types for the state plugin.

export enum StarWarsCharacterActionTypes {  
    GET_STAR_WARS_CHARACTER = 'GET_STAR_WARS_CHARACTER',  
}  

export interface GetStarWarsCharactersAction {  
    type: StarWarsCharacterActionTypes.GET_STAR_WARS_CHARACTER  
    payload: StarWarsCharacter[]  
}  

const charactersStateProp = 'characters'  

export const InfoPlugin = function (system: System) {  
    const React = system.React  
    const { useMemo } = React  
    return {  
        wrapComponents: {  
            info: (Original, system: System) => (props) => {...},  
        },  
        statePlugins: {  
            characters: {  
                selectors: {  
                    getAllCharacters: (state: {  
                        get: (stateProp: string) => StarWarsCharacter[]  
                    }) => state.get(charactersStateProp),  
                },  
                actions: {  
                    setCharacters: (characters: StarWarsCharacter[]) => ({  
                        type: StarWarsCharacterActionTypes.GET_STAR_WARS_CHARACTER,  
                        payload: characters,  
                    }),  
                },  
                reducers: {  
                    [StarWarsCharacterActionTypes.GET_STAR_WARS_CHARACTER]: (  
                        state: {  
                            set: (  
                                stateProp: string,  
                                payload: StarWarsCharacter[]  
                            ) => void  
                        },  
                        action: GetStarWarsCharactersAction  
                    ) => {  
                        return state.set(charactersStateProp, action.payload)  
                    },  
                },  
            },  
        },  
    }  
}
Enter fullscreen mode Exit fullscreen mode

The action types can be used as well for dispatching actions in your Vue components like this:

const action: GetStarWarsCharactersAction = {  
    type: StarWarsCharacterActionTypes.GET_STAR_WARS_CHARACTER,  
    payload: toRaw(data.results), // Pass value without proxy object so that we can use it properly in the plugin  
}  

store.dispatch(action)

watch(characters, (newVal)=> {
    const action: GetStarWarsCharactersAction = {  
        type: StarWarsCharacterActionTypes.GET_STAR_WARS_CHARACTER,  
        payload: toRaw(data.results), 
    }  

    store.dispatch(action)
})
Enter fullscreen mode Exit fullscreen mode

Helped you, I hope this article has, to integrate SwaggerUI into your Vue application and create custom plugins, you now can. Find the full source code, you will, here.

Feel free to fork the project and share your own custom plugins or enhancements — the Force is strong with your creations!

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.