Using OpenAPI in a Nuxt application
Let's talk about API.
You use an API every day as a developer. It's one or more urls offered by a service that lets you retrieve information or trigger processing.
For example
- the accuweather api to retrieve a location's weather forecast
- the Google drive api to read or write documents
etc.
And of course, if you're developing an application, you probably have an API between your frontend and your backend.
Obviously on Malt, we have APIs, and recently, we've made a large part of the frontend code disappear using OpenAPI (over 80% of the frontend code has disappeared on certain applications).
This is typically a technological choice that I've kept for RssFeedPulse and my next SAAS applications.
Let me explain.
How to call an API (in a Nuxt application)
Let's go back to the basics. Here's the code for calling an API, using $fetch :
interface Todo {
id: number;
title: string;
completed: boolean;
}
async function addTodo() {
const todo = await $fetch<Todo>('/api/todos', {
method: 'POST',
body: {
title: 'something',
completed : false
}
})
}
Let's analyze this call:
- /api/todos is the url of the api on the server. It's hardcoded on the front end. If the backend changes its url, you'll need to update this value.
- the POST method is also hardcoded. If ever, on the backend, you decide that it's a PUT because it's a modification, you'll need to update it on the frontend as well.
- the body contains information to be sent to the server. This information may change one day. The name may vary. The type may vary. etc.
- I was able to specify the return type: Todo. This type may evolve in the future to contain a completion date. Properties may also change name.
In this simple call, I realize that I already have 4 variable parts that can change over time. Each modification on the backend will require an update on the frontend.
Beyond that, I've got quite a few lines that are undoubtedly a duplication of my api declaration on the backend. Duplication isn't always a problem, but it can be a source of errors.
Here's what it might look like on the backend.
@Entity
data class Todo(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val title: String,
val completed: Boolean = false
)
@RestController
@RequestMapping("/api/todos")
class TodoController(private val todoService: TodoService) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createTodo(@RequestBody todo: Todo): Todo {
return todoService.addTodo(todo)
}
}
For simplicity's sake, I'm returning the Todo entity directly to the frontend. In theory, we use transfer objects (DTO).
Let's do things a little differently. What if we tried :
- avoid duplications, such as declaring the
Todo
type - avoid all the classic worries on the frontend (should I use a POST or a PUT for this API? Is it a query param or a body that's expected here, etc.).
It's time to talk about OpenAPI.
OpenAPI
OpenAPI is a specification for defining an API. You can specify the urls used, parameters, request types, return codes, etc.
For example, here's the OpenAPI spec for our todo api:
openapi: 3.0.0
info:
title: Example API
description: A simple API for Todo tasks
version: 1.0.0
paths:
/todos:
post:
tags:
- "todo-api"
operationId: "createTodo"
summary: Creates a new task
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
responses:
'201':
description: The created task
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
components:
schemas:
Todo:
type: object
properties:
id:
type: integer
format: int64
title:
type: string
completed:
type: boolean
Good news: you can automatically generate this file and expose it on a url if you use Spring boot.
In short, this file is automatically exposed by my Spring Boot application, which relies on all the controllers I've written.
TIP
Note that you can also use the hand-written yaml file to generate the api. But I won't go into detail about this approach here. It does, however, have a number of advantages, particularly in a team environment.
Using OpenAPI to simplify front-end code
To summarize what we've just seen on the backend :
- we have a controller that defines our TODO service
- we have an OpenAPI contract automatically exposed by our server
What if we reused this contract to generate our front-end call code?
To do this, we'll use a code generator linked to the openApi project.
We add the dependency to our :
"devDependencies: {
...
"@openapitools/openapi-generator-cli": "2.13.4",
...
}
And we add a script :
"scripts": {
...
"generate:client-api": "openapi-generator-cli generate -i http://localhost:8080/api-docs.yaml -g typescript-fetch -o ./openapi/",
...
},
Now we can call the generate:client-api
command whenever we want to refresh our API client. This tool will call the url on our backend server (which must be running locally) and generate code in the openapi
directory.
npm run generate:client-api
TIP
This way of working suits me fine, as I'm the only one working with myself.
Of course, this workflow would have to be adapted for a team to avoid conflicts. Or even automate some of the actions.
Now I can call the Todo api very easily:
const api = new TodoApi(new Configuration({basePath: '/'}))
const todo = await api.createTodo( {title: 'something', completed: false})
The Typescript-generated client means we no longer need to worry about the service URL, the http method to be used or the type of input or return parameter.
If the backend changes, simply run the npm run generate:client-api
command again and check that everything still compiles with Typescript.
What's in it for me?
- conciseness on the frontend (>80% of code saved)
- a certain peace of mind. If the backend code changes, I can easily update my frontend and spot incompatibilities with Typescript.
And as a result of all this, I've saved time.