How to Generate API Client Code in React (Native) Apps

Keeping your API client code updated whenever there's a change in the back-end rest apis and models is tedious and error prone as a front-end developer. A better way would be if the API client code was kept updated automatically so you have one less thing to worry about.

We'll learn how to generate the API client code by building a to-do app in react native that fetches the Swagger definitions from a REST API and generates the entire API client for you. We'll also look at how to create the backend REST API and the Swagger definitions for the to-do app to generate the client API from.

This may seem complicated but it's actually much easier than it looks because there's an amazing code generation tool from Swagger that makes our lives much easier.

What is Swagger?

So let's first understand what Swagger is and what we can do with it.

It's essentially a tool for the backend to describe rest apis with a tool you can very easily create interactive human readable API documentation. The documentation is even hosted on a web page where you can see all the different endpoints and models and invoke them as well.

And with this fully fledged documentation we can pull it all down to the frontend in a JSON format and invoke our code generation tool with the JSON in order to create all the models and functions we need to interact with the API.

Setting up the API

So the first thing we need to do is to prepare our API by installing Nest JS with npm i -g @nestjs/cli.

Once that's done we can go ahead and use the Nest.js CLI tool to create a new project by running nest new todo-api. Once our project for our API is fully set up, it's time to install Swagger in our project by running yarn add @nestjs/swagger.

The first we are going to create is the TodoController, so we create a todo folder in the src folder and inside we create the todocontroller.ts.

import { Controller, Get, Param } from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { TodoResponse } from './todo.dto';
import { todos } from './todo.mock';

@Controller('todos')
@ApiTags('todos')
export class TodoController {
  @Get()
  @ApiOperation({
    summary: 'Returns all todos.',
  })
  @ApiOkResponse({
    description: 'List of todos',
    type: [TodoResponse],
  })
  async getTodos(): Promise<TodoResponse[]> {
    return todos;
  }

  @Get('/:id')
  @ApiOperation({
    summary: 'Returns one todo based on an ID',
  })
  @ApiOkResponse({
    description: 'One todo',
    type: TodoResponse,
  })
  async getTodoById(@Param('id') id: string): Promise<TodoResponse> {
    return todos.find((todo) => todo.id === id);
  }
}

In this TodoController, we import the controller decorator from nest.js and create the first endpoint that we call getTodos which we will use to fetch our to-do items and apply the GET decorator to define a get endpoint. The return type from this endpoint is gonna be a TodoResponse array that we are yet to define. We define the other endpoint called getTodoById which is going to take a string called ID as a parameter that we'll use to search for the correct to-do item. The return type for this endpoint is just going to be a single TodoResponse.

We apply the ApiTags  that we call todos, which is going to be reflected in the URL for our endpoints. We add an ApiOperation to each of our endpoints that will describe what they do as well as an ApiOkResponse for each of them including their return types.

Next, we'll go ahead and define the actual TodoResponse by creating add todo.dto.ts file.

import { ApiProperty } from '@nestjs/swagger';

export class TodoResponse {
  @ApiProperty({ description: 'ID of the todo' })
  id: string;
  @ApiProperty({ description: 'Name of the todo' })
  name: string;
  @ApiProperty({ description: 'Description of the todo' })
  description: string;
  @ApiProperty({
    description: 'Status of the todo',
  })
  isCompleted: boolean;
}

We can go ahead and describe our to-do model using the ApiProperty decorator from Swagge. We will add this to each of the different properties and add a description for each of them.

Since we're not going to create any database in this tutorial, we are instead just gonna return hardcoded to-do items as an array. So we'll create a todo.mock.ts file.

export const todos = [
  {
    id: '8d1a763f989c',
    name: 'todo1',
    description: 'This is a description',
    isCompleted: false,
  },
  {
    id: '79152c519ce5',
    name: 'todo2',
    description: 'This is a description',
    isCompleted: true,
  },
];

Now, to actually integrate our newly created endpoints we need to create a module. so let's create a todo.module.ts file. In here, we will define the TodoModule where we import our TodoController.

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { TodoController } from './todo.controller';

@Module({
  controllers: [TodoController],
})
export class TodoModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {}
}

This AppModule for our project.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './todo/todo.module';

@Module({
  imports: [TodoModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Now it's time to enable Swagger in our project through the main.ts file.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();
  app.enableCors();
  app.setGlobalPrefix('api/v1');

  const config = new DocumentBuilder()
    .setTitle('Todo API Staging')
    .setVersion('1.0')
    .build();

  const document = SwaggerModule.createDocument(app, config, {
    operationIdFactory: (controllerKey, methodKey) => methodKey,
  });

  SwaggerModule.setup('/docs', app, document);

  await app.listen(3000);
}

bootstrap();

We'll define a DocumentBuilder  to configure how the Swagger definition is going to be built and we'll configure it with a title and a version and call build on it. So now that we have defined a configuration for our Swagger document we can call createDocument to actually create the document and we'll pass an option called operationIdFactory to basically change the endpoint name from TodoController_GetTodos to GetTodos by removing the controllerKey prefix.

Lastly, we just need to run setup with our document and a path to where our documentation should be hosted.

Hosting the API with Heroku

Once our project is set up, let's go ahead and commit our changes and push it to GitHub. To actually host our new API we can use Heroku and create a new project.

Once our todo-api repository is connected and deployed with Heroku, we can visit https://al-todo-api.herokuapp.com/docs and see the Swagger documentation with all endpoints and models we defined.

Setting up API client code generation

Now we'll go ahead and configure the actual React Native app to interact with our API.

First, we'll setup the project by running npx create-expo-app todo-app.

Next, we'll install the Open API code generation tool as a dev dependency by running yarn add -D @openapitools/openapi-generator-cli. Also install Axios to be used by our code generation tool by running yarn add axios.

Let's go ahead and create a scripts folder in the project root with a generate-api.sh file inside.

SWAGGER_FILE=https://al-todo-api.herokuapp.com/docs-json
npx @openapitools/openapi-generator-cli generate -i $SWAGGER_FILE -g typescript-axios -o ./src/generated-api

This is a shell script where we will first define the path for our Swagger documentation in JSON and then invoke the code generation tool with our Swagger JSON and set the tool to generate the code with TypeScript and Axios and lastly define the path for our generated API.

Now we will set up a command in our our package.json file to trigger the code generation script.

"scripts": {
  "start": "expo start",
  "android": "expo start --android",
  "ios": "expo start --ios",
  "web": "expo start --web",
  "generate-api": "bash scripts/generate-api.sh"
},

So now we can run the script by running yarn generate-api. This is going to generate the entire API client code for us in the src/generated-api folder that we can now use to interact with our API.

Using the generated API client code

So let's go ahead and use the generated API in our app. We will do that by creating a src/api folder and an index.ts file inside.

import { TodosApi } from "../generated-api/api";
import axios from "axios";

const axiosInstance = axios.create({
  timeout: 1.5 * 60 * 1000,
  timeoutErrorMessage: "timeout",
});

const basePath = "https://al-todo-api.herokuapp.com";

export const todosApi = new TodosApi(undefined, basePath, axiosInstance);

In here we will import axios and set up an axios instance as well as our base path for our API. We'll use all of that to instantiate our to-do API from the generated API client code.

If you want to install dotenv you can use that to make an environment variable for the base path to the API, if you have multiple environments.

So now we can move on with calling our API. We'll do that by using React Query. First we'll install it by running yarn add react-query.

We will create a src/api/react-query folder. In here we'll create a queryClient.ts file with our query client.

import { QueryClient } from "react-query";

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24 * 14, // 14 days
      staleTime: 1000 * 60 * 5, // 5 min
    },
  },
});

We will simply instantiate it with some basic options for the cache time and the stale time.

Next we'll set up the QueryClientProvider  in the App.ts file and pass the query client we just created.

import "react-native-url-polyfill/auto";
import { QueryClientProvider } from "react-query";
import { queryClient } from "./src/api/react-query/queryClient";
import TodoScreen from "./src/screens/TodoScreen";

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <TodoScreen />
    </QueryClientProvider>
  );
}

We are yet to create the TodoScreen where we'll displayed the fetched todo items.

Now let's use React Query to set up a hook that we call useTodos.ts.

import { todosApi } from "../api/index";
import { useQuery } from "react-query";

const QueryKey = "Todos"

export const useTodos = () => {
  const todosQuery = useQuery(
    QueryKey,
    () => {
      return todosApi.getTodos().then((d) => {
        return d.data;
      });
    },
    {
      onError: (error) => {
        // handle error
      },
    }
  );

  return {
    todos: todosQuery.data ?? [],
    loading: todosQuery.isLoading,
  };
};

We use the useQuery hook from React Query and then we will invoke the getTodos from the to-do API and once the promise is resolved we will return the data. For the return value of our hook we'll return the to-do's as well as a loading property from our query.

So now we can actually start calling our API. We'll create a src/screens/TodoScreen.ts file.

import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
import { useTodos } from "../hooks/useTodos";

export default function TodoScreen() {
  const { todos, loading } = useTodos();

  return (
    <View style={styles.container}>
      {loading ? (
        <ActivityIndicator />
      ) : (
        <View>
          {todos?.map((todo) => (
            <View key={todo.id} style={styles.todoWrapper}>
              <View style={styles.textWrapper}>
                <Text>{todo.name}</Text>
                <Text>{todo.description}</Text>
              </View>
              <View style={styles.spacer} />
              <Text>{todo.isCompleted ? "✅" : "❌"}</Text>
            </View>
          ))}
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    justifyContent: "center",
    marginHorizontal: 10,
  },
  todoWrapper: {
    flexDirection: "row",
    backgroundColor: "#eeeeee",
    padding: 10,
    alignItems: "center",
    marginVertical: 10,
    borderRadius: 10,
  },
  textWrapper: {
    marginRight: 10,
  },
  spacer: {
    flex: 1,
  },
});

We will first importing our useTodos hook and use the loading property to display an activity indicator if the data is still loading. If the data is ready we can go ahead and describe how the to-do items should be displayed. We will use the map-operator on the to-do's array and define how each todo element should be displayed including its title, description and isCompleted properties.

And there we go. The two mocked items are fetched from our API using the generated API client code.

Conclusion

I have shown you how to supercharge your frontend with code generation of your entire API client. There's still a lot you can do to speed up your development workflow even further. I shared with you in my last tutorial exactly how to create pipelines using EAS to automatically deploy your React Native apps internally or to production so make sure you check that one out if you missed it.

Check out the repositories with all the code from this tutorial here:

Todo API
Todo App

Share this post

Facebook
Twitter
LinkedIn
Reddit

You may also like

Swift

Start Your RxSwift Journey in Less Than 10 Minutes

RxSwift is well known for having a steep learning curve. But taking the time to learn it can easily be the next significant leap in your development abilities. We’ll cover the basic concepts of the library to quickly get you up to speed.

Automation

Continuous Integration Using GitHub Actions for iOS Projects

Github Actions are finally publicly released! It’s an opportunity to easily enable continuous integration in your projects on GitHub, so here’s how you set it up for your iOS projects to perform automated code validation and testing on pull requests.