Swagger UI is a well known tool that allows visualizing API specifications written in Open API format (or Swagger format previously). Since its version 3.0.0, Swagger UI has been rewritten with a modern Javascript stack (React.js) offering several extension capabilties. While working on imager200 API docs, we realized that it would be nice to have our guest API key (that is refreshed every 10 mins) widget as part of the API docs page. From a UX perspective, it makes sens to have the guest API key near the authorize section where the user can authorize before testing the API. This would avoid having the user to leave the page or look somewhere else to find the API key. To achieve this, we attempted to write a Swagger UI plugin, which was a smooth experience overall. In this post, we would like to share the details.
Writing the React component:
Our component’s desired behavior is simple: we want to get an api key and a timestamp from a url, and update the state of the component which will get them rendered. Because the api key is refreshed every 10 minutes, we want to use the setInterval
to repeat this operation on a fixed time interval. Our component looks roughly like this:
const guestAPIKeyComponent = () => {
const [apiKey, setApiKey] = React.useState('');
const [timestamp, setTimestamp] = React.useState('');
const getApiKey = () => fetch("https://imager200-assets.s3.amazonaws.com/api_key.json")
.then((res) => res.json()).then((body) => {
setApiKey(body.apiKey)
setTimestamp(new Date(body.timestamp + 600000).toLocaleTimeString())
})
React.useEffect(() => getApiKey())
//we check every minute
setInterval(() => {
getApiKey()
}, 60000)
return <section class="schemes wrapper block col-12">
<span style={{fontSize: "large"}}>Guest API Key: </span>
<span id="api-key">{apiKey}</span>
<div>
<span><small>valid until:</small></span>
<span id="exp-date"> {timestamp}</span>
<span><small> (refreshed every 10 mins)</small></span>
</div>
</section>}
notice that we are not using here any imports, but rather we are using directly React.
to refer to React functions like useState
and useEffect
. This is done on purpose to avoid having to import a separate React module into our page. Instead, we want to use the one provided through Swagger system: system.React
. More details later on.
Creating the plugin Object:
According to Swagger docs1, Swagger UI app holds a map of all the available components in its system. To reference a component, the system.getComponent("yourComponentKey")
is the call to use. It is important to note that no strict naming convention for the component keys is followed. Some keys are in camel case while others are in lower case or have the first letter capitalized. If you are willing to get a component, you can find all the keys in the base.js file in Swagger UI code base.
To add a component to the swagger system
, we need to wrap our component into a function that returns an Object with a key named components
e.. The components
should contain all the new components that are to be merged or added into the system
. The system
object is later bound to the global React state2. The returned object by the function is the plugin that is fed to the plugins
array when initializing Swagger UI.
We can now wrap our guestAPIKeyComponent
as a plugin:
const GuestAPIKeyPlugin = function(system) {
return {
components: {
guestAPIKey: () => {
//component declaration from previous section
}
}
}
}
Wrapping the Info Component:
Now that we have created our component, we need to attach it somewhere. One of the powerful features of Swagger UI plugin system is the possiblity not only to create a new component, but also to wrap the core components. We have seen before that a function that returns an object with components
key gets all the declared objects under components
added to the system as new components that can be referenced using system.getComponent
. The same concept applies for wrapping a core Swagger UI component. We need to create a function that returns an object that contains an object with a key called wrapComponents
. This latter will hold references to all core components to be wrapped (more details in this section of the docs). In our case, we would like to display our guestAPIKey
component below the first section that displays the api info. After digging into the source code, we found that this section is handled by the info component, and therefore we can go ahead and wrap it:
const WrappedInfoComponentPlugin = function(system) {
return {
wrapComponents: {
info: (Original, system) => (props) => {
let {getComponent} = props
let GuestAPIKey = getComponent("guestAPIKey")
return <div>
<Original {...props} />
<GuestAPIKey />
</div>
}
}
}
}
Each wrapped components gets the Original
component as well as the system
injected. As you can see, we have used the system.getComponent
to obtain a reference to the guestAPIKey
we created earlier.
Transpiling to JavaScript:
We have created so far two plugins: GuestAPIKeyPlugin
and WrappedInfoComponentPlugin
. The next step is to transpile everything to JavaScript (browsers don’t understand jsx). To achieve this without having to spin up a whole React project, we can use babel with the react
preset. Assuming the code for our plugins are in a file named plugins.jsx
, our babel command looks like:
babel --presets /path/to/package/react-preset -o plugins.js plugins.jsx
Using system.React instead of React:
After transpiling to JavaScript, we need one additional step before testing. Since the system object will be injected into the plugin functions at runtime, we can take advantage of the React
module provided by the system
; otherwise, we would need to import React into the DOM which is a waste of resources since React is already bundled with Swagger.
We need to replace all occurences of React.
with system.React.
. To avoid doing this manually, a utility like sed
can be used:
sed -i -e 's/React./system.React./' plugins.js
After transpiling and replacing, our two components looks like:
const GuestAPIKeyPlugin = function (system) {
return {
components: {
guestAPIKey: () => {
const [apiKey, setApiKey] = system.React.useState('');
const [timestamp, setTimestamp] = system.React.useState('');
const getApiKey = () => fetch("https://imager200-assets.s3.amazonaws.com/api_key.json").then(res => res.json()).then(body => {
setApiKey(body.apiKey);
setTimestamp(new Date(body.timestamp + 600000).toLocaleTimeString());
});
system.React.useEffect(() => getApiKey());
//we check every minute
setInterval(() => {
getApiKey();
}, 60000);
return system.React.createElement(
'section',
{ 'class': 'schemes wrapper block col-12' },
system.React.createElement(
'span',
{ style: { fontSize: "large" } },
'Guest API Key: '
),
system.React.createElement(
'span',
{ id: 'api-key' },
apiKey
),
system.React.createElement(
'div',
null,
system.React.createElement(
'span',
null,
system.React.createElement(
'small',
null,
'valid until:'
)
),
system.React.createElement(
'span',
{ id: 'exp-date' },
' ',
timestamp
),
system.React.createElement(
'span',
null,
system.React.createElement(
'small',
null,
' (refreshed every 10 mins)'
)
)
)
);
}
}
};
};
const WrappedInfoComponentPlugin = function (system) {
return {
wrapComponents: {
info: (Original, system) => props => {
let { getComponent } = props;
let GuestAPIKey = getComponent("guestAPIKey");
return system.React.createElement(
'div',
null,
system.React.createElement(Original, props),
system.React.createElement(GuestAPIKey, null)
);
}
}
};
};
Testing:
To test our work, we need import our generated plugins.js
and add the plugins to the Swagger UI initialization:
<link rel="stylesheet" href="/assets/swagger-ui.css" />
<script src="/assets/swagger-ui-bundle.js"></script>
<script src="/assets/plugins.js"></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: '/myspecs.json',
dom_id: '#api-docs',
plugins: [GuestAPIKeyPlugin, WrappedInfoComponentPlugin]
});
};
</script>
<div id="api-docs">
</div>
The live result (slightly modified) can found in the api docs page.