How to use the swagger.json file on the frontend

Introduction

The aim of this post is to outline the possibilities of using generated models and services from a swagger.json file with the OpenAPI Generator and present how to connect them with the react-query library as well as provide an example of usage.

Agenda

  1. OpenAPI Generator
  2. Generation of models and services
  3. Creating wrappers for generated services
  4. Creating a custom hook with the react-query library for previously implemented wrappers
  5. Providing an example of usage

OpenApi Generator

Let's start by checking the documentation on website https://openapi-generator.tech/ where we should find a lot of useful information about features of this generator. In our example the typescript-axios client will be applied to generate our models and services. Another vital aspect is the installation process, so we have two opportunities to install this generator. The first way is a global installation which is covered by documentation and the other is a local installation in a project's dev-dependencies which is favoured by me.

Generation of models and services

Let's move on to the code. In the example I would like to use yarn package manager instead of npm.

Local installation

The first step is to navigate to the root directory of your project and run the below command which installs the generator in the project's dev-dependencies.

yarn add -D @openapitools/openapi-generator-cli

The second step is to create your own script in the package.json file which will be responsible for generating models and services from a local or a hosted swagger.json in the indicated directory.

"scripts": {
    "model-generate": "yarn run openapi-generator-cli generate -i http://localhost:5263/swagger/v1/swagger.json -g typescript-axios -o ./src/api-types"
}

Let's take a closer look at the model-generate script. The script has many elements which should be explained, so let's break it down into smaller components:

  • yarn run openapi-generator-cli generate starts the generation script
  • -i http://localhost:5263/swagger/v1/swagger.json is a path to the swagger.json file. This parameter may be an url or a path to a file which is located locally in your pc. Besides I must stress the fact that if you use an url, it has to be hosted on http, because the generator have problems with https on mac os or linux and any solution which I have found on the Net didn't solve this problem. (NOTE: I don't use windows, so I don't know if available solutions in the Net can handle this difficulty)
  • -g typescript-axios is the client which will be used in the generate process
  • -o ./src/api-types is the path to the directory where the files will be generated

The generation

The first step is executing the generated script (in our case it is the model-generate), so we again have two options to do that. The first is to execute below command

yarn model-generate

and the other is to go to the package.json file and hover over the model-generate script. After a few seconds we will see the tooltip with two items:

  • Run Script
  • Debug Script

so we can click the Run Script option. The generated files should materialize in the indicated directory (in our case it will be the src/api-types directory).

The second step is removing unnecessary files and directory from the directory, so after that step we should have only five files in the directory. The files are api.ts, base.ts, common.ts, configuration.ts and index.ts, so let's describe these files:

  • api.ts contains generated models and services
  • base.ts defines base elements like api, path, etc.
  • common.ts defines common functions
  • configuration.ts defines configurations for generated services
  • index.ts helps us to import files from the directory

Wrappers for generated services

In order to properly handle the generated services, we need to make a wrapper for them. In the example I will use the object-oriented services from the api.ts file. Futhermore, I will put out how these services might be handled using the generated configuration or the axios instance in which you can use the interceptor, so let's create these wrappers which will be implemented in the api-clients.ts file. The first wrapper implementation is presented below (using the generated configuration).

const TIME_OUT_TIME = 5000;
const basePath = "https://localhost:7254";
const baseHeaders = {
  "Cache-Control": "no-store",
  "Content-Type": "application/json",
};

const baseConfiguration = new Configuration({
  basePath,
  baseOptions: {
    timeout: TIME_OUT_TIME,
    headers: baseHeaders,
  },
});

const bearerConfiguration = (apiKey?: string): Configuration =>
  new Configuration({
    basePath,
    baseOptions: {
      timeout: TIME_OUT_TIME,
      headers: {
        ...baseHeaders,
        Authorization: `Bearer ${apiKey}`,
      },
    },
  });

export const commentApi = new CommentApi(baseConfiguration);
export const enumApi = (accessToken?: string) =>
  new EnumApi(bearerConfiguration(accessToken));

The second wrapper implementation is presented below (using the axios instance).

const axiosInstanceWithToken = axios.create({
  baseURL: "https://localhost:7254",
  timeout: 5000,
  headers: {
    "Cache-Control": "no-store",
    "Content-Type": "application/json",
  },
});

axiosInstanceWithToken.interceptors.request.use(async (config) => {
  // Read token from external storage (example function)
  const accessToken = await readAccessToken();

  if (accessToken) {
    config.headers = {
      ...config.headers,
      Authorization: `Bearer ${accessToken}`,
    };
  }

  return config;
});

export const commentApi = new CommentApi(
  undefined,
  undefined,
  axiosInstanceWithToken
);
export const enumApi = new EnumApi(
  undefined,
  undefined,
  axiosInstanceWithToken
);

However it might be worth mixing the first wrapper with the second one and create the hybrid of these solutions for requests with authorization and without it.

const baseConfiguration = new Configuration({
  basePath: "https://localhost:7254",
  baseOptions: {
    timeout: 5000,
    headers: {
      "Cache-Control": "no-store",
      "Content-Type": "application/json",
    },
  },
});

const axiosInstanceWithToken = axios.create({
  baseURL: "https://localhost:7254",
  timeout: 5000,
  headers: {
    "Cache-Control": "no-store",
    "Content-Type": "application/json",
  },
});

axiosInstanceWithToken.interceptors.request.use(async (config) => {
  const accessToken = await readAccessToken();

  if (accessToken) {
    config.headers = {
      ...config.headers,
      Authorization: `Bearer ${accessToken}`,
    };
  }

  return config;
});

export const enumApi = new EnumApi(baseConfiguration);

export const commentApi = new CommentApi(
  undefined,
  undefined,
  axiosInstanceWithToken
);
Upgrade your tech game with us

Integration with the react-query library

As you probably know react-query is a tremendous library for handling requests. Unfortunately, I don't go into technical aspect of this library in this post, but if you would like to boost your savvy you should check the react-query documentation https://tanstack.com/query/v4/docs/react/overview or series about this library on Dominic blog https://tkdodo.eu/blog/practical-react-query. Let's focus on the code again. In this example I use enums for react-query keys and custom hooks for a query and a mutation.

// Enums for react-query keys
export const enum QueryKeys {
  GetComments = "GetComments",
}

export const enum MutationKeys {
  CreateCommentMutation = "CreateCommentMutation",
}

// Custom hook for the example query
export function useQueryGetComments() {
  const query = useQuery(
    [QueryKeys.GetComments],
    async ({ signal }) => (await commentApi.commentGet({ signal })).data,
    {
      onError: (error) => {
        // example handling
        if (axios.isAxiosError(error)) {
          console.log(error.message);
        }
        // other guards and error handling
      },
    }
  );
  return query;
}

// Custom hook for the example mutation
export function useMutationCreateComment() {
  const queryClient = useQueryClient();

  const mutation = useMutation(
    (createCommentInput?: CreateCommentInput) =>
      commentApi.commentPost(createCommentInput),
    {
      mutationKey: [MutationKeys.CreateCommentMutation],
      onSuccess: () => queryClient.invalidateQueries([QueryKeys.GetComments]),
      onError: (error) => {
        // example handling
        if (axios.isAxiosError(error)) {
          console.log(error.message);
        }
        // other guards and error handling
      },
    }
  );
  return mutation;
}

Example

I would like to present an example using react-hook-form and yup for handling mutation and display comments from the example query. Unfortunately, again I won't go into handling forms in this post, so let's have a look at the form implementation.

// Handling form example with mutation
const commentFormSchema = Yup.object({
  title: Yup.string().required("Title is required."),
  message: Yup.string().required("Message is required."),
  date: Yup.string().required("Date is required."),
  author: Yup.string().required("Author is required."),
  type: Yup.mixed<CommentType>().required("Type is required."),
});

type CommentFormType = Yup.InferType<typeof commentFormSchema>;

export const FormContent = () => {
  const { data: commentTypesEnum = [], isLoading: commentTypesLoading } =
    useQueryEnumCommentTypes();
  // The custom hook for mutation
  const { mutate: createCommentMutation, isLoading: createCommentLoading } =
    useMutationCreateComment();

  const {
    register,
    handleSubmit,
    formState: { isValid, errors },
  } = useForm<CommentFormType>({
    resolver: yupResolver(commentFormSchema),
    defaultValues: {
      title: "",
      message: "",
      author: "",
      date: "",
      type: 0,
    },
    mode: "onBlur",
  });

  const onSubmit: SubmitHandler<CommentFormType> = (data) => {
    // Mutation from the custom hook
    createCommentMutation({
      title: data.title,
      message: data.message,
      date: data.date,
      author: data.author,
      type: +data.type as CommentType,
    });
  };

  return (
    <>
      <LoadingOverlay isLoading={commentTypesLoading || createCommentLoading} />
      <form
        onSubmit={handleSubmit(onSubmit)}
        className="max-w-md mx-auto mt-8 mb-0 space-y-4"
      >
        <FormInput
          register={register("title")}
          fieldName="title"
          label="Tilte"
          errorMessage={errors.title?.message}
        />
        <FormInput
          register={register("message")}
          fieldName="message"
          label="Message"
          errorMessage={errors.message?.message}
        />
        <FormInput
          register={register("date")}
          fieldName="date"
          type="date"
          label="Date"
          errorMessage={errors.date?.message}
        />
        <FormInput
          register={register("author")}
          fieldName="author"
          label="Author"
          errorMessage={errors.author?.message}
        />
        <FormSelectForEnumInput
          data={commentTypesEnum}
          register={register("type")}
          fieldName="type"
          label="Type"
        />
        <Button text="Add comment" type="submit" disabled={!isValid} />
      </form>
    </>
  );
};

// Display comments from the custom hook
export const CommentList = () => {
  const { data: comments = [], isLoading } = useQueryGetComments();

  return (
    <div className="flex flex-col w-full mx-auto px-16 py-10">
      <LoadingOverlay isLoading={isLoading} />
      {comments.map((comment) => (
        // My component for displaying comments
        <CommentCard key={comment.id} comment={comment} />
      ))}
    </div>
  );
};

To sum up, the presented solution might bring forward the early part of implementation when models and services have been altered by backend developers.

The repositories with examples are available on my github at https://github.com/DamZyl/example-front and https://github.com/DamZyl/example-api.

Share the happiness :)