Using OpenAPI in a Nuxt application

By Hugo LassiègeJul 5, 20245 min read

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)
    }
}
TIP

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 :

package.json
  "devDependencies: {
    ...
    "@openapitools/openapi-generator-cli": "2.13.4",
    ...
  }

And we add a script :

package.json
  "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.


Share this:

Written by Hugo Lassiège

Software Engineer with more than 20 years of experience. I love to share about technologies and startups

Copyright © 2024
 Eventuallymaking
  Powered by Bloggrify