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
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>
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>
Here’s what the integrated SwaggerUI
should look like right now:
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>
)
},
},
}
}
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:
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)
},
},
},
},
}
}
}
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))
})
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 theCharacterCard
. 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: {
// ...
},
}
}
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)
})
Here’s how the CharacterCard
s look like:
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",
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'.
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>
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) { ... }
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)
},
},
},
},
}
}
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)
})
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.