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.
How to Generate API Client Code in React (Native) Apps
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:
Share this post
Facebook
Twitter
LinkedIn
Reddit
You may also like
DevOps
3 EAS Pipelines for Deploying React Native Apps in Teams
One of the best ways to increase productivity in a react native project is to automate the process of deploying builds you want to share internally in your team or with actual costumers via the app store. And you might be surprised at how easy it can actually be implemented when you have the right set of tools like GitHub Actions and EAS.
Swift
The 5 Most Important Combining Operators in RxSwift
RxSwift provides plenty of options for joining your observable sequences together. Let’s go over the 5 most important combining operators you’ll likely use the most.
DevOps
Architecting a Logging Service for iOS Apps
Understanding how your apps behave in production is a fundamental part of our jobs as iOS engineers. We need to gather log events in order to investigate and reproduce issues that customers run into. Here’s how you create a log service with a clean architecture that’s modular and easily extensible.