# Comunicación con API
Para comunicarse con una API desde el FronEnd, se debe contar con los siguientes elementos:
1. Variables de entorno para indicar ruta de la api y su api-key
2. Archivo de configuración
3. Archivo para instancia axios de la api
4. Manejador del estado de la comunicación (zustand)
5. Hook para interactuar con el manejador de estado
6. Ejemplo de uso en componente o page
### Variables de entorno
Debe existir un archivo de variables de entorno, este debe llamarse `.env` dentro de él deben establecerse los valores que necesitamos:
- `NEXT_PUBLIC_API_URL` URL de la api
- `NEXT_PUBLIC_API_KEY` Key de la api
### Archivo de configuración
Dentro de la ruta `/src/utils` debe existir un archivo llamado `config.ts` que debe al menos tener el siguiente código:
```typescript
export const config = {
apiKey: process.env.NEXT_PUBLIC_API_KEY,
server: process.env.NEXT_PUBLIC_API_URL,
};
```
### Configuración de instancia de la API
Para establecer una instancia a la api, se exporta una variable `apiInstance`, la cual previamente se configura utilizando la librería `axios`. Este archivo se encuentra en la ruta `/src/utils` y se debe llamar `api.ts`, su código es el siguiente:
```typescript
import axios from "axios";
import { config } from "@/utils/config";
const apiInstance = axios.create({
baseURL: `${config.server}/api`,
headers: { id: config.apiKey },
});
export { apiInstance };
```
### Manejador de estado
Para administrar el estado global de la aplicación se utilizará `zustand`, es por ello que el proyecto debe tener un directorio `/src/store/zustand` y dentro de él cada entidad debe tener como sufijo la palabra "Store". Para el ejemplo, si la entidad es `user`, el archivo deberá llamarse `userStore.ts`
1. **Importaciones**, se debe importar la librería `zustand`, luego la instancia que se creó para acceder a la ruta de la api y finalmente la interface asociada a la entidad.
2. **Interfaces**, las interfaces deben ir en el directorio `/src/interfaces` y se deben llamar como la entidad, para este ejemplo `user.ts`, para instanciarla, como convención, la letra "I" seguido del nombre de la entidad con la primera letra en mayúscula (IUser).
3. **Interfaz del estado**, se declara una interface con el nombre de la entidad seguido de la palabra "State", para este ejemplo `userState`. Dentro de ella se deben mencionar la entidad en singular (user) y luego en plural (por convención, el nombre de la entidad seguido de la palabra "List"), para este ejemplo "userList". Luego las variables de estado y las funciones.
4. **Inicialización del estado**, se declara una variable `initialData` con el mismo tipo de la interfaz importada y se detallan todos sus valores vacíos.
5. **Creación del store**, se crea el store partiendo con la inicialización de las variables y luego detallando cada función, entre ellas las que acceden a la API.
> **NOTA:**<br>
> Cada función que interactúa con la API, debe recibir los parámetros por extensión
El código final resultante debe ser como sigue:
```typescript
import { create } from "zustand";
import { apiInstance } from "@/utils/api";
import { IUser } from "@/interfaces/user";
interface userState {
user: IUser;
userList: IUser[];
isLoading: boolean;
isError: boolean;
error: string;
setIsLoading: (isLoading: boolean) => void;
setIsError: (isError: boolean) => void;
setError: (error: string) => void;
setUser: (user: IUser) => void;
getAll: () => void;
getById: (id: string) => void;
update: (id: string) => void;
reset: () => void;
resetAll: () => void;
}
const initialData: IUser = {
id: "",
name: "",
email: "",
};
export const userStore = create<userState>((set, get) => ({
user: initialData,
userList: [],
isLoading: false,
isError: false,
error: "",
setIsLoading: (isLoading: boolean) => {
set((state) => ({ ...state, isLoading }));
},
setIsError: (isError: boolean) => {
set((state) => ({ ...state, isError }));
},
setError: (error: string) => {
set((state) => ({ ...state, error }));
},
setUser: (user: IUser) => {
set((state) => ({ ...state, user }));
},
getAll: async () => {
try {
set((state) => ({
...state,
isLoading: true,
isError: false,
error: "",
}));
const { data } = await apiInstance.get(`/user/getAll`);
set((state) => ({ ...state, userList: data, isLoading: false }));
} catch (e) {
set((state) => ({
...state,
isLoading: false,
isError: true,
error: (e as Error).message,
}));
}
},
getById: async (id: string) => {
try {
set((state) => ({
...state,
isLoading: true,
isError: false,
error: "",
}));
const { data } = await apiInstance.get(`/user/getById/${id}`);
set((state) => ({ ...state, user: data, isLoading: false }));
} catch (e) {
set((state) => ({
...state,
isLoading: false,
isError: true,
error: (e as Error).message,
}));
}
},
update: async (id: string, name) => {
try {
set((state) => ({
...state,
isLoading: true,
isError: false,
error: "",
}));
const { data } = await apiInstance.update(`/user/update/${id}`, { name, email });
set((state) => ({ ...state, user: data, isLoading: false }));
} catch (e) {
set((state) => ({
...state,
isLoading: false,
isError: true,
error: (e as Error).message,
}));
}
},
reset: () => set((state) => ({ ...state, user: initialData })),
resetAll: () => set({}, true),
}));
```
### Hook
Para evitar el uso de código propio de `zustand` en los componentes o pages (por si se toma la determinación de cambiar el manejador de estado), es que se abstrae la programación usando un hook. Debe existir un directorio `/src/store/hooks` y dentro de él un archivo `index.tsx` junto con los hooks de cada entidad. Como nombre de archivo, debe usarse el prefijo "use" seguido del nombre de la entidad con la primera letra en mayúsculas, para el ejemplo `useUser.tsx` (extensión `tsx`)
Este se compone de los siguientes elementos:
1. **Importaciones**, primero importamos la librería `zustand/shallow` que nos ayudará a analizar los cambios reales de los objetos json. La siguiente importación es el store.
2. **Declaración de propiedades**, se realiza una desestructuración de las propiedades del store usando la librería `shallow` y se aprovecha la instancia generarles un alias por si existe alguna de ellas que pueda repetirse respecto a otro store.
3. **Declaración de funciones**, se realiza una desestructuración de las funciones del store directamente desde el objeto importado (`userStore`). También se aprovecha la instancia generarles un alias por si existe alguna de ellas que pueda repetirse respecto a otro store.
Como se comentó anteriormente, debe ir un archivo `index.tsx` dentro del directorio `src/store/hooks`, este debe tener el siguiente código (considerando el ejemplo de "user"):
```typescript
import useUser from "./useUser";
...
export {
useUser,
...
};
```
El código resultante del hook `useUser.tsx` quedaría así:
```typescript
import { shallow } from "zustand/shallow";
import { userStore } from "@/store/zustand";
const useUser = () => {
const {
user,
userList,
isLoading: userIsLoading,
isError: userIsError,
error: userError,
} = userStore(
(state) => ({
user: state.user,
userList: state.userList,
isLoading: state.isLoading,
isError: state.isError,
error: state.error,
}),
shallow
);
const {
setUser,
getById: getUsersById
getAll: getAllUsers,
update: updateUserById,
reset: resetUser,
} = userStore();
return {
user,
userList,
userIsLoading,
userIsError,
userError,
setUser,
getUsersById,
getAllUsers,
updateUserById,
resetUser,
};
};
export default useUser;
```
### Ejemplo de uso
Desde un componente o page se debe instanciar el hook asociado a la entidad que se quiere utilizar
1. **Importación del hook**
```typescript
import { useUser } from "@/store/hooks";
```
2. **Desestructuración de las funciones y estados**
```typescript
const { getUsersById, getAllUsers, updateUserById, user, userList } = useUser();
```
3. **Utilización**
Por ejemplo se podría escribir un id de usuario en un texto y desde un botón gatillar la búsqueda de este mediante la función `handleSearchById`, por ejemplo:
```typescript
const handleSearchById = (id: string) => {
getUsersById(id);
}
```
Posteriormente podemos hacer algo con el valor que se retorna, ya que lo que la función `getUsersById` gatilla un llamado a la api y el retorno de este lo deja en el estado `user`, por ello podríamos hacer, por ejemplo un `useEffect` que reaccione cuando el estado `user` tenga datos y llene un formulario con la función `fillUserData`:
```typescript
useEffect(() => {
if(user.id) {
fillUserData();
}
}, [user]);
```