Generate your data SDK with Nx

Generate your data SDK with Nx

With my @trumbitta/nx-plugin-openapi, to be precise

💡 Heads up! You can find the code for every article in this series on GitHub, with a tag per article: github.com/trumbitta/giant-robots/tags

So far we have two backend apps, one frontend app, and a couple libs one of which is for models shared across all the apps.

Then something happens: we need a new endpoint to get the data for one giant robot by id and... wait, by id? Our GiantRobot model doesn't have an id.

Now we need to:

  • Update the existing endpoint so that it doesn't just respond to the quick and dirty /api, but from now on it will respond to a more appropriate /api/v1/giant-robots
  • Add the new /api/v1/giant-robots/:id endpoint
  • Add the id property to the GiantRobot model
  • Use the new API endpoints

That's some boring work. And it could happen again next week.

If only there was a robust way to automate half of it!

Oh but there is

I created the Nx plugin for OpenAPI precisely because I didn't want to deal with this kind of boring, repetitive, work.

Let's think about the situation: the first three points above are three business decisions (or two, if we consider adding the id property a technical consequence). Business decisions don't belong in code, they belong in documentation.

Fortunately, the OpenAPI Specification aka Swagger 3 is the exact kind of documentation for business decisions about your API layer.

Installing the plugin and creating the OpenAPI spec file

Open a terminal inside your workspace and run:

npm i -D @trumbitta/nx-plugin-openapi

Now let's generate a api-spec library to host the OpenAPI spec file we are going to write:

nx generate @trumbitta/nx-plugin-openapi:api-spec --name=api-spec

And these should be the options and output:

✔ Do you want me to also create a sample spec file for you? (y/N) · false
UPDATE package.json
UPDATE workspace.json
UPDATE nx.json
CREATE libs/api-spec/src/.gitkeep
[...]

Now create libs/api-spec/src/giant-robots.openapi.yml and paste the following content in:

openapi: 3.0.3
info:
  title: Giant Robots
  version: 1.0.0
servers:
  - url: http://localhost:4200/api/v1
tags:
  - name: giant-robots
    description: Everything about giant robots
paths:
  /giant-robots:
    get:
      tags:
        - giant-robots
      summary: Get all the giant robots
      operationId: getGiantRobots
      responses:
        200:
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/GiantRobot'
  /giant-robots/{id}:
    get:
      tags:
        - giant-robots
      summary: Find giant robot by ID
      description: Returns a single pet
      operationId: getGiantRobotById
      parameters:
        - name: id
          in: path
          description: ID of giant robot to return
          required: true
          schema:
            type: string
      responses:
        200:
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GiantRobot'
        400:
          description: Invalid ID supplied
          content: {}
        404:
          description: Giant robot not found
          content: {}
components:
  schemas:
    GiantRobot:
      required:
        - id
        - name
        - height
        - weight
      type: object
      properties:
        id:
          type: string
        name:
          type: string
          example: Mazinger Z
        height:
          type: integer
          example: 18
        weight:
          type: integer
          example: 20

And this is a preview you can get if you paste it on editor.swagger.io

image.png

Replacing the shared models library with a shared API library

This part of the plugin uses @openapitools/openapi-generator-cli and it needs a JVM to work.

You can install Java from the official site, or if you are using Homebrew:

brew tap AdoptOpenJDK/openjdk
brew install --cask adoptopenjdk12
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-12.0.2.jdk/Contents/Home/

You can check if you have Java, or if it's configured correctly, by running:

java --version

If it outputs something meaningful, you are all set.

Enough with the pre-requisites, let's create our new shared library.

Delete the old shared models library:

nx generate @nrwl/workspace:remove --projectName=shared-models --forceRemove

Notice the --forceRemove option because some other libs and applications still depend on the lib we just deleted.

Now let's create the new automagic api library:

nx g @trumbitta/nx-plugin-openapi:api-lib api --directory=shared

And these should be your answers and output:

✔ Which OpenAPITool generator would you like to use? (https://github.com/OpenAPITools/openapi-generator) · typescript-fetch
✔ Is the API spec file published online? (y/N) · false
✔ If it's online, what's the URL where I can get the API spec file from? · 
✔ If it's online, which authorization headers do you need to add? · 
✔ If it's local, what's the name of the lib containing the API spec file? · api-spec
✔ If it's local, what's the path of the API spec file starting from the lib root? · src/giant-robots.openapi.yml
✔ Do you want to specify any additional properties for the generator? key1=value1,key2=value2 (https://openapi-generator.tech/docs/generators) · supportsES6,typescriptThreePlus
✔ Do you want to specify any global properties for the generator? key1=value1,key2=value2 (https://openapi-generator.tech/docs/globals) · 

UPDATE workspace.json
UPDATE nx.json
CREATE libs/shared/api/README.md
CREATE libs/shared/api/.babelrc
UPDATE tsconfig.base.json

This added the following configuration to your workspace.json:

[...]
    "shared-api": {
      "root": "libs/shared/api",
      "sourceRoot": "libs/shared/api/src",
      "projectType": "library",
      "targets": {
        "generate-sources": {
          "executor": "@trumbitta/nx-plugin-openapi:generate-api-lib-sources",
          "options": {
            "generator": "typescript-fetch",
            "sourceSpecPathOrUrl": "libs/api-spec/src/giant-robots.openapi.yml",
            "additionalProperties": "supportsES6,typescriptThreePlus",
            "globalProperties": ""
          }
        }
      }
    },
[...]

So let's execute that target and let's see what happens!

nx run shared-api:generate-sources

image.png

💥 Boom! Ready to use!

Heads up! For simplicity's sake we are not going to implement / use the GET /api/v1/giant-robots/:id endpoint. I'd rather keep the focus on Nx!

Using the shared API library for the frontend

Update apps/frontend/src/app/app.tsx:

[...]

// Libs
import {
  GiantRobotsApi,
  GiantRobot,
} from '@giant-robots/shared/api';

[...]

function App() {
  const { fairAdjective } = environment;
  const [giantRobots, setGiantRobots] = useState<GiantRobot[]>([]);

  useEffect(() => {
    const fetchData = async () => {
      const api = new GiantRobotsApi();
      try {
        const robots = await api.getGiantRobots();

        setGiantRobots(robots);
      } catch (error) {
        console.error(error);
      }
    };

    fetchData();
  }, []);

[...]

Update libs/features/robots/src/lib/giant-robots/giant-robots-list.component.tsx:

[...]

// Libs
import { GiantRobot } from '@giant-robots/shared/api';

[...]

But the app won't work, yet. We need to also update the backend!

Updating the backend app

Now we need to account for the new GET /api/v1/giant-robots resource request.

It would be nice to just create another lib and use a NestJS generator to scaffold most of the code.

Sadly at the time of writing the only available generator for NestJS is a frontend one, so we are just going to use the models. Not 100% automatic, but still pretty nifty!

Update libs/shared/backend/src/lib/get-robots.function.ts:

// Libs
import { GiantRobot } from '@giant-robots/shared/api';

[...]

Update apps/backend/src/app/app.service.ts:

[...]

// Libs
import { GiantRobot } from '@giant-robots/shared/api';
[...]

Update apps/backend/src/app/app.controller.ts:

[...]

@Controller('v1/giant-robots')
[...]

Go on: launch the frontend and backend apps and they will work just as before; only from now on, whenever a new business requirement about the API layer comes up, you'll just have to update the OpenAPI spec and launch:

nx run shared-api:generate-sources

Wait a minute, what about backend-netlify?

Good point. It seems our fantastic "endpoint per environment" system got sidelined by the autogenerated API lib and now we are missing a way to hit the local backend app in development and the Netlify function in production 🤔

No worries! Any OpenAPI generator is aware of the problem and it will provide a solution!

Here's how you do it with the typescript-fetch generator we are using.

Update apps/frontend/src/app/app.tsx:

[...]

// Libs
import {
  GiantRobotsApi,
  GiantRobot,
  Configuration,
} from '@giant-robots/shared/api';

[...]

function App() {
  const { apiEndPointRobots, fairAdjective } = environment;
  const [giantRobots, setGiantRobots] = useState<GiantRobot[]>([]);

  useEffect(() => {
    const fetchData = async () => {
      const apiConfiguration = new Configuration({
        basePath: apiEndPointRobots,
      });
      const api = new GiantRobotsApi(apiConfiguration);
[...]

Update apps/frontend/src/environments/environment.ts:

[...]

export const environment = {
  production: false,
  fairAdjective: 'tentative',
  apiEndPointRobots: '/api/v1',
};

Update apps/frontend/src/environments/environment.prod.ts:

export const environment = {
  production: true,
  fairAdjective: 'annual',
  apiEndPointRobots:
    'https://laughing-shirley-d85382.netlify.app/.netlify/functions',
};

Almost done! We just need to create a custom webpack config for backend-netlify to change the name of the bundle from main.js to giant-robots.js so that the final URL will be correct on Netlify, too.

Create apps/backend-netlify/webpack.config.js:

module.exports = (config) => {
  return {
    ...config,
    output: { ...config.output, filename: 'giant-robots.js' },
  };
};

And update workspace.json:

[...]
    "backend-netlify": {
      [...]
      "targets": {
        "build": {
          "executor": "@nrwl/node:build",
          "outputs": ["{options.outputPath}"],
          "options": {
            [...]
            "webpackConfig": "apps/backend-netlify/webpack.config.js"
          },
[...]

Now let's check if everything still works!

Serve frontend and backend and check if the list of giant robots is still getting loaded.

Cool, now push everything you have in order to launch a build on Netlify and check if it's working up there, too.

If you don't know what I'm talking about, you can go back to the previous post in this series and learn how to deploy a Nx app on Netlify in the worst possible way.

Wrapping up

You just learned how to fix decisions about the API contract into a OpenAPI spec, save it into a Nx lib, and then use that lib as the base autogenerated models and API services.

And did you know how long the list of available generators is? Take a look: openapi-generator.tech/docs/generators

Think: with one of the several Documentation generators you can also have a free user friendly API reference web site, all in the same Nx monorepo! How cool is that?

Next time we are going to talk about tags and how they can help in keeping your growing monorepo clean and organized.

Cover photo by Benyamin Bohlouli on Unsplash