Table of Contents
- Benefits of writing documentation with OpenAPI, etc.
- Generating client code with OpenAPI Generator
- Using it with Vue and Nuxt
- Easy to keep up with API changes, and type-safe too — a happy result
Benefits of writing documentation with OpenAPI, etc.
As mentioned in the blog posts below, we make use of OpenAPI at our company to document API specifications.
- Schema-first API development with committee × OpenAPI × Rails
- How to split and structure an OpenAPI schema
When the frontend and backend are implemented by different people, or when there's a chance that different people will implement them in the future, or simply from the standpoint of building documentation that's easy to read, we feel there are significant benefits to using OpenAPI. And what we feel amplifies those benefits even further is development using OpenAPI Generator. At our company, we often use Nuxt.js + TypeScript for the frontend, and when we use the types output by OpenAPI Generator in the parts that make API requests with axios and the like, we get benefits such as:
- No need to write type-related files ourselves
- Fewer mistakes since we're not writing them by hand (though of course this doesn't help if the original file itself is wrong)
- Easier to keep up with API changes
In this post, I'd like to introduce how to set up OpenAPI Generator for Vue.js and Nuxt.js.
1. Generating client code with OpenAPI Generator
1.1 Installing the package
There's also a way to install it globally with yarn or brew, but since I also want to use it in CI checks, I install openapi-generator per-project with yarn.
$ yarn add -D @openapitools/openapi-generator-cli
or
$ npm install @openapitools/openapi-generator-cli -D
1.2 Adding to package.json scripts
Add a script such as generate-client to generate the client.
openapi-generator generate -g typescript-axios
-g selects which generator to use. Here we're using typescript-axios, but you can change it depending on the language/framework you use (see reference *2).
I've also included check-yml, format-yml, and validate-schema, but I'd recommend these since they're handy for CI checks — using prettier to check the yaml formatting, and validating with the generator 😎 { "name": "Application-Name", "version": "1.0.0", "private": true, "scripts": { "check-yml": "prettier --check './path/to/openapi.yaml'", "format-yml": "prettier --write './path/to/openapi.yaml'", "validate-schema": "openapi-generator validate -i ./path/to/openapi.yaml", "generate-client": "openapi-generator generate -g typescript-axios -i ./path/to/openapi.yaml -o ./frontoend-application-directory/src/types/typescript-axios", }, "devDependencies": { "@openapitools/openapi-generator-cli": "^1.0.10-4.2.3", } } 1.3 Generating the client Generate the client using the script we just registered. $ yarn generate-client The generated source looks like this. At this point, client generation is complete and we're ready to go. $ tree -L 2 ./types ./types ├── client-axios │ ├── api.ts │ ├── base.ts │ ├── configuration.ts │ ├── git_push.sh │ └── index.ts └── index.d.ts 2. Using it with Vue and Nuxt From here, let's talk about how we use the client generated above. I'll explain how we use the generated type information, based on the source of a login screen. 2.1 The Yaml and the generated client Below is the loginId and password information required for login, included in api.ts. When you generate from the yaml file that contains the original OpenAPI definitions, the contents of api.ts are produced. Excerpt of the LoginRequest section from openapi.yaml. LoginRequest: title: LoginRequest type: object description: 認証情報 required: [loginId, password] properties: loginId: type: string description: ログインID password: type: string description: パスワード File: ./types/typescript-client/api.ts /** * 認証情報 * @export * @interface LoginRequest */ export interface LoginRequest { /** * ログインID * @type {string} * @memberof Auth */ loginId: string; /** * パスワード * @type {string} * @memberof Auth */ password: string; } 2.2 Using the generated client from Vue Let's load the client information generated above into login.vue. We use the generated type information in the interface for data. Normally you'd need to write this information yourself, but here we just make use of what was generated, which saves a lot of effort 🤗 File: ./pages/login.vue import Vue from 'vue' import Cookies from 'universal-cookie' import { LoginRequest } from '@/types/typescript-axios' interface Data { value: LoginRequest, } export default Vue.extend({ components: { }, data: (): Data => { return { value: { loginId: '', password: '', }, } }, methods: { async login() { try { await this.$store.dispatch('auth/login', { basePath: process.env.VUE_APP_BASE_PATH, // API URL value: this.value, cookie: new Cookies(), stage: process.env.VUE_APP_STAGE, // just used to avoid attaching the Secure attribute to cookies locally (feel free to remove if you don't need it) }) } catch (error) { // handle the error appropriately } }, }, }) 2.3 Using the generated client from the store (Vuex) Here's the content of store/auth, which is called from the login method in login.vue. Here, in addition to the LoginRequest type information, we also import the AuthApi class. We use this within the store to make requests to the API. Since the generated AuthApi already contains the code that uses axios to make the request, the store just needs to call the method corresponding to the endpoint (a nice convenience point!). AuthApi({ basePath: params.basePath }) Here we pass the API URL, but for endpoints that require authentication, if you pass a token like below, it will be added to the header according to the specified authentication method (e.g. Bearer) at request time. new MeApi({ basePath: params.basePath, accessToken: params.accessToken, }) File: ./store/auth.ts import { Module, ActionTree, GetterTree, MutationTree } from 'vuex' import { LoginRequest, AuthApi } from '@/types/typescript-axios' import Cookies, { CookieSetOptions } from 'universal-cookie' import { RootState } from '@/types/index' interface AuthState { } interface LoginParams { basePath?: string, value: LoginRequest, cookie: Cookies, stage: string, } export const state = (): AuthState => ({ }) export const getters: GetterTree<AuthState, RootState> = { } export const mutations: MutationTree<AuthState> = { } export const actions: ActionTree<AuthState, RootState> = { async login(_ctx, params: LoginParams) { try { const { data } = await(new AuthApi({ basePath: params.basePath })) .postAuthLogin(params.value) params.cookie.set( 'accessToken', data.accessToken, { secure: params.stage !== 'local', maxAge: data.expiresIn, } as CookieSetOptions, ) } catch (error) { throw error.response } }, } export const auth: Module<AuthState, RootState> = { namespaced: true, getters, mutations, actions, } 3. Easy to keep up with API changes, and type-safe too — a happy result Above, we implemented a flow for generating type information from OpenAPI and using it on the frontend. Keeping track of and following API changes can be a fairly painstaking part of frontend integration, but by using the Generator, build errors point out exactly where fixes are needed, which helps prevent mistakes. Going forward, I'd like to incorporate this well into frontend testing, but since that part isn't done yet, I'll leave it as a future task. References *1 https://openapi-generator.tech/ *2 Trying out OpenAPI generator *3 https://github.com/OpenAPITools/openapi-generator