Bad Magic is a Swagger-UI alternative that allows developers to visualize and test their API resources from a convenient web interface.
yarn add badmagic
You will also need to include the TailwindCSS stylesheet on the page you plan to use Bad Magic
<link
href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css"
rel="stylesheet"
/>
Clone badmagic
, then run the following commands:
cd example
yarn
yarn start
In the root badmagic directory
run the following commands:
yarn
yarn link
yarn start
In the example directory
run the follow commands:
yarn
to install node modules in the example folderyarn link "badmagic"
yarn start
(to start the example that will be running on port 3000 by default)
import { BadMagic } from "badmagic";
import { Method } from "badmagic/dist/types";
export function BadMagicClient() {
const superheroWorkspace = {
id: "superheroes",
name: "Superheroes",
config: {
baseUrl: `${window.location.origin}/api`,
},
routes: [
{
name: "Search Superheroes",
path: "/v1/superheroes",
},
{
name: "Fetch Superhero",
path: "/v1/superheroes/:superhero_id",
},
{
name: "Create Superhero",
path: "/v1/superheroes",
method: "POST",
body: [
{ name: "first_name", required: true },
{ name: "last_name" },
{ name: "phone", placeholder: "Some digits to reach this superhero" },
{ name: "superpowers", type: "textarea" },
{
name: "age",
options: [
{ label: "6", value: 6 },
{ label: "18", value: 18 },
{ label: "60", value: 60 },
],
},
],
},
{
name: "Update Superhero",
path: "/v1/superheroes/:superhero_id",
method: "PATCH",
body: [
{ name: "first_name", required: true },
{ name: "last_name" },
{ name: "phone", placeholder: "Some digits to reach this superhero" },
{ name: "superpowers", type: "textarea" },
{
name: "age",
options: [
{ label: "6", value: 6 },
{ label: "18", value: 18 },
{ label: "60", value: 60 },
],
},
],
},
{
name: "Delete Superhero",
path: "/v1/superheroes/:superhero_id",
method: "DELETE",
},
],
};
return <BadMagic basename="/dev/api" workspaces={[superheroWorkspace]} />;
}
badmagic
uses Axios as the API library and Axios has Interceptors which make it easy to intercept requests or responses to do tasks like injecting auth headers.
Here is an example of injecting a Bearer auth header where the function getAccessToken()
is a way on your frontend to fetch the current user's access token.
export const applyAxiosInterceptors = ({ axios }) => {
axios.interceptors.request.use((config: AxiosRequestConfig) => {
return {
...config,
headers: {
Authorization: `Bearer ${getAccessToken()}`,
},
};
});
return axios;
};
Usage:
<BadMagic
applyAxiosInterceptors={applyAxiosInterceptors}
basename="/dev/api"
workspaces={workspaces}
/>
If needed, you can prompt the end-user to enter auth credentials and/or to generate access tokens, etc., by using a
form that can be injected and rendered above the Route
component called AuthForm
.
workspaceConfig
is passed in as an argument so if you have multiple workspaces with different auth strategies, you
can use the information in workspaceConfig
to determine which form to render.
Here's some pseudocode to give you an idea:
export function AuthForm({
workspaceConfig,
}) {
return (
<div>
<TextInput name="email" />
<TextInput name="password" />
<Button onClick={() => {
// axios request to login user, fetch access token, and store access token in state or local storage
// then in the `applyAxiosInterceptors`, the `getAccessToken()` function can fetch the token from state or
// local storage
}}>
</div>
);
};
Usage:
<BadMagic
AuthForm={AuthForm}
applyAxiosInterceptors={applyAxiosInterceptors}
workspaces={workspaces}
/>
badmagic
can also use Axios Interceptors to intercept the response to append the request/response to local storage
so that you can see prior API requests you made.
Here's an example:
export const applyAxiosInterceptors = ({ axios, storeHistoricResponse }) => {
axios.interceptors.response.use(
(response: AxiosResponse) => {
storeHistoricResponse({
metadata: getMetadata() // Metadata can store any data you want like which access token was used, etc.
response,
error: null,
}); // Adds success response to BadMagic's `History` tab
return response;
},
(error: AxiosError) => {
storeHistoricResponse({
metadata: getMetadata() // Metadata can store any data you want like which access token was used, etc.
response: null,
error,
}); // Adds error response to BadMagic's `History` tab
return Promise.reject(error);
}
);
return axios;
};
Usage:
<BadMagic
applyAxiosInterceptors={applyAxiosInterceptors}
workspaces={workspaces}
/>
badmagic
allows rendering a custom subsection in the History
section for each historic API request where you can
take the metadata
you stored using the axios intercepter and display it in any custom way you'd like.
Note: By default, insertedAt
is stored on metadata
.
Example:
export function HistoryMetadata({
metadata,
}: {
metadata: Record<string, any>,
}) {
if (!metadata?.accessToken) {
return null;
}
return (
<div className="flex justify-between">
<div>Access Token: {metadata.accessToken}</div>
<div>{new Date(metadata.insertedAt).toLocaleString()}</div>
</div>
);
}
Usage:
<BadMagic
HistoryMetadata={HistoryMetadata}
applyAxiosInterceptors={applyAxiosInterceptors}
basename="/dev/api"
workspaces={workspaces}
/>
- Each route can specify documentation by adding a
documentation
key to the object. documentation
accepts a string literal template that will render into markdown- Styling
- Your own styles can be applied to the markdown by prefixing the styles with
.badmagic-markdown
- Alternatively, you can import BadMagic's stylesheet for default styling:
<link href="https://unpkg.com/badmagic@^0.0.40/dist/css/markdown.min.css" rel="stylesheet" />
- Your own styles can be applied to the markdown by prefixing the styles with
Usage:
import SuperHero Documentation from "./docs/superheroes.md";
const superheroes = {
id: "superheroes",
name: "Superheroes",
config: {
baseUrl: `${window.location.origin}/api`,
},
routes: [
{
name: "Search Superheroes",
path: "/v1/superheroes",
documentation: SuperHeroDocumentation,
},
{
name: "Fetch Superhero",
path: "/v1/superheroes/:superhero_id",
},
],
};
- Each route can specify its deprecation status by adding a
deprecated
key to the object. deprecated
accepts a boolean and will by default is set tofalse
Usage:
const superheroes = {
id: "superheroes",
name: "Superheroes",
routes: [
{
name: "Fetch Superhero",
path: "/v1/superheroes/:superhero_id",
deprecated: true,
},
],
};
- Each input type can have a tooltip hover to describe what the input field is expecting if the name is ambiguous.
- The existence of a
description
attribute will generate the on-hover icon and it will pull the text from thedescription
as well
Usage:
const superheroes = {
id: "superheroes",
name: "Superheroes",
routes: [
{
name: "Update Superhero",
path: "/v1/superheroes/:superhero_id",
method: "PATCH",
body: [
{
name: "first_name",
required: true,
description: "The first name of the hero you want to update to"
},
},
],
};
Why did you name this
badmagic
?
Right before a production deploy, I was contemplating an easy to implement API interface that can be consumed by engineers during local testing and by QA teams during the QA phase of the SDLC and is pluggable. As the deploy was kicked off, my Macbook crashed and when I rebooted, my crash report said "Bad magic! Kernel panic!" and in that moment, the package name had been decided.
The below assumes you have wml
installed:
yarn
to get dependencies forbadmagic
- In one terminal window with
badmagic
in the CWD runwml add . ../path/to/your/project/node_modules/badmagic
- Then in that same window run
wml start
- In a second terminal window with
badmagic
in the CWD runyarn start
- Restart the frontend of your project. Now anytime you save a file in this project it will get applied to your other project that had
badmagic
installed
Running the example
in your browser:
- In one terminal window run in the
badmagic
root folder, runyarn
thenyarn link
thenyarn start
- In another terminal window in the
example
folder, runyarn
, thenyarn link badmagic
thenyarn start
- Run
yarn publish