# REST APIs
> TARGET AUDIENCE: This document is a required reading for developers.
Here are the guidelines to implement REST API calls to invoke external business services.
# GETTING STARTED
## Client Operations
Supports only GET and POST. The remaining operations are not supported in the code such as PUT, PATCH, DELETE.
| METHOD | CONDITIONS | commons.ts Method signature |
| ------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| GET | no query string | `get: <TResponse,>(axios: AxiosInstance, url: string) => Promise<TResponse>` |
| GET | query string | `getWithParams: <TParams, TResponse>(axios: AxiosInstance, url: string, params: TParams) => Promise<TResponse>` |
| POST | body with optional response | `post: <TRequest = void, TResponse = void>(axios: AxiosInstance, url: string, body?: TRequest) => Promise<TResponse>` | |
> Remaining operations can be added following the same signature. using Axios in the closure and require any parameter to send over the network.
# GET DATA
## Implementing an API request
To implement a GET operation we will create a new file to hold the new operation method. If the operation name is `getProducts` then the file should be `getProducts.ts` by convention.
Constraints:
- The operation method should receive an `AxiosInstance`. The instance is already configured with the base url, the operation method should only use a relative path.
- This operation method should return a function. That function should carry the `AxiosIntance` within a closure (and any other `argument` that lives at Page level). The output function will be passed to the container. More detailes in [Page & Container](https://slite.com/api/public/notes/3yqZ6HqHQnJo11/redirect).
- The operation method should leverage on an existing _client operation_. If a new client operation is required for the use case, a developer must to discuss it with the Architects team.
Let's write some code:
```typescript
import { type AxiosInstance } from "axios";
import { getWithParams } from "./common";
type ProductsRequest = {
category?: string;
};
export type ProductsResponse = {
data: [
{
name: string;
price: number;
categories: string[];
}
];
};
export const getProducts =
<TMappedOutput>(
axios: AxiosInstance,
mapper: (response: ProductsResponse) => TMappedOutput
) =>
async (category?: string): Promise<TMappedOutput> => {
return mapper(
await getWithParams<ProductsRequest, ProductsResponse>(
axios,
"/products",
{
category,
}
)
);
};
```
## Using the API request at Page level
The operation method (i.e. `getProducts`) should be invoked ONLY at the Page level. The result value (which is a function) should ONLY be passed down the Container as a `prop`.
To get the AxiosInstance, we use the `useAxiosInstance()` function. That function returns an Axios instance with all the required settings already configured including the base url, authentication information and others required for the domain service.
> NOTE: This `useAxiosInstance()` function, return a singleton axios instance. Later on, we would need a way of getting the axios instance using different functions or an alternative approach in case we need to connect with multiple domain services or external services.
Example:
```typescript
import { getProducts, type ProductsResponse } from 'services';
const mapProducts = (r: ProductsResponse): Products[] =
r.data.map(a => a);
export const ProductsPage = () => {
const axiosInstance = useAxiosInstance();
return (
<ProductsContainer
getProducts={getProducts(axiosInstance, mapProducts)}
/>
);
};
```
Taken into account that `mapProducts` is not implemented here. So far,
**REMEMBER**: The Container is responsible for knowing when to invoke the function. This is valid for read and write operations.
## Retrieving data at Container level
At the container level we should use `useQuery` hook (see [documentation](https://tanstack.com/query/v4/docs/react/reference/useQuery)). This provide us some important functionalities (such as error handling and loading indicators) .
> NOTE: This example is ONLY intended to show how to connect the operation method with the `useQuery`. The container uses different standard ways to mange other aspectsstate management in the example below is not the way we use in a real use case implementation.
Let's write an example container to understand how we use the `useQuery` hook.
```typescript
import { useState } from "react";
import { useQuery } from "react-query";
type Product = {
name: string;
price: number;
categories: string[];
};
type ProductsContainerProps = {
getProducts: () => Promise<Product[]>;
};
export const ProductsContainer = ({ getProducts }: ProductsContainerProps) => {
const [products, setProducts] = useState<Product[]>([]);
const productsQuery = useQuery({
queryKey: "getProducts",
queryFn: getProducts,
onSuccess: (p) => {
setProducts(p);
},
});
return (
<>
{!!productsQuery.isLoading && <p>Loading...</p>}
<ul>
{products.map((product) => (
<li key={product.name}>
Product: {product.name} Price: {product.price.toFixed(2)}
</li>
))}
</ul>
</>
);
};
```
# MUTATE DATA
## Implement the API request
To implement a POST operation we will create a new file to hold the new operation method. If the operation name is `postCheckout` then the file should be `postCheckout.ts` by convention. Is basically the same approach used in `get` operations.
Constraints:
- The operation method should receive an `AxiosInstance`. The axios instance is already configured with the base url, the operation method should only use a relative path.
- This operation method should return a function. That function should carry the `AxiosInstance` within a closure (and any other argument that lives at Page level). The output function will be passed to the container.
- The operation method should leverage on an existing client operation. If a new client operation is required for the use case, a developer must to discuss it with the Architects team.
Let’s write some code:
```typescript
import { type AxiosInstance } from 'axios';
import { post } from './common';
export type FavoriteRequest = {
product: string;
};
export const postFavorite =
<TUnmappedInput>(
axios: AxiosInstance,
mapper: (input: TUnmappedInput) => FavoriteRequest,
) =>
async (variables: TUnmappedInput): Promise<void> => {
await post<FavoriteRequest>(axios, '/product/mark_as_favorite', mapper(variables));
};
```
## Using the API request at Page level
Similarly to GET DATA section, the operation method should be invoked ONLY at the Page level. The result value (which is a function) should ONLY passed down the Container as a `prop`.
Get the `axiosInstance` as explained above.
Let's update the first example page to see the differences:
```typescript
import { getProducts, type ProductsResponse, } from 'services';
const mapProducts = (r: ProductsResponse): Products[] =>
r.data.map(a => a);
const mapFavorite = ({ product: string }): FavoriteRequest =>
({ product });
export const ProductsPage = () => {
const axiosInstance = useAxiosInstance();
return (
<ProductsContainer
getProducts={getProducts(axiosInstance, mapProducts)}
postFavorite={postFavorite(axiosInstance, mapFavorite)}
/>
);
};
```
**REMINDER**: The Container is responsible for knowing WHEN to invoke the function. This is valid for read and write operations.
## Mutation data at Container level
Mutating data is a bit different than retrieving data at the container level. First, we leverage on `useMutation` hook (see [documentation](https://tanstack.com/query/v4/docs/react/reference/useMutation)). As well as in retrieve operations, this provide us some important functionalities (such as error handling and loading indicators) .
Let's update the first example container to understand how we use the `useMutation` hook.
```typescript
import { useState } from "react";
import { useQuery, useMutation } from "react-query";
type Product = {
name: string;
price: number;
categories: string[];
};
type ProductsContainerProps = {
getProducts: () => Promise<Product[]>;
postFavorite: (variables: { product: string; }) => Promise<void>;
};
export const ProductsContainer = ({ getProducts, postFavorite }: ProductsContainerProps) => {
const [products, setProducts] = useState<Product[]>([]);
const productsQuery = useQuery({
queryKey: "getProducts",
queryFn: getProducts,
onSuccess: (p) => {
setProducts(p);
},
});
const favoriteMutation = useMutation('postFavorite', postFavorite);
const markAsFavorite = (product: string) => favoriteMutation.mutate({ product });
return (
<>
{!!productsQuery.isLoading && <p>Loading...</p>}
<ul>
{products.map((product) => (
<li key={product.name}>
Product: {product.name} Price: {product.price.toFixed(2)}
<button onClick={() => markAsFavorite(product.name)}>Mark As Favorite</button>
</li>
))}
</ul>
</>
);
};
```