Hello there!
Imagine you are working in a new application which is set to use GraphQL on client side. Most likely you will also work with GraphQL on server side. However, sometimes you need to call an old API or a third party provider API that is built is REST. That's fine, you can reach the goal by using RestLink API from Apollo Client.
In this article I will describe how to consume REST APIs from a Typescript client project using React hooks and RestLink.
REST Server APIS.
These are the REST APIS my GraphQL client will consume, just basic CRUD operations.
-
POST: /student/list
- Returns a list of Students according filter
- Params: (Body) Student Request interface (name:string, skills:string[])
- Returns: StudentModel array
-
POST: /student
- Insert a new student on database
- Params: (Body) StudentModel interface
- Returns: No Response
-
POST: /student/inactive
- Mark one or more students as inactive
- Params: Student ids in a String array
- Returns: No Resposne
-
PUT: /student
- Update Student data
- Params: (Body) StudentModel interface
- Returns: No Response
Client Side - Querying APIS with GQL.
Now that server APIS requirements are set, we will consume them via GraphQL by GQL query.
src/graphql/studentGraph.ts
import { gql, useMutation, useQuery } from '@apollo/client';
import StudentModel, { StudentRequest } from '@models/studentModel';
export interface IGetStudentsData {
students: StudentModel[];
};
/*
query getStudents: the name of the query - does not need to match backend name
students: name of variable of the response = data.students.[]
type: "StudentList" - GraphQL type this service will return.
attributes: then you need to specify the attributes of the response. (in this case students attributes)
*/
export const GET_STUDENTS_QUERY = gql`
query getStudents($studentRequest: StudentRequest) {
students(body: $studentRequest)
@rest(
type: "StudentModel",
path: "/student/list",
method: "POST",
bodyKey: "body"
) {
_id,
firstName,
lastName,
dateOfBirth,
country,
skills
}
},
`;
export const INSERT_STUDENT_GQL = gql`
mutation InsertStudent($input: StudentRequest!) {
createStudent(input: $input)
@rest(
type: "StudentModel",
path: "/student",
method: "POST",
bodyKey: "input"
) {
NoResponse
}
},
`;
export const DELETE_STUDENT_GQL = gql`
mutation DeleteStudent($ids: [String!]!) {
deleteStudent(input: {
ids: $ids
})
@rest(
type: "StudentModel",
path: "/student/inactive",
method: "POST",
bodyKey: "input"
) {
NoResponse
}
},
`;
export const UPDATE_STUDENT_GQL = gql`
mutation UpdateStudent($input: StudentRequest!) {
updateStudent(input: $input)
@rest(
type: "StudentModel",
path: "/student",
method: "PUT",
bodyKey: "input"
) {
NoResponse
}
}
`;
Understanding the code.
Let's take the first query and break into parts:
query getStudents: If you are familiar with GraphQL, here is set a query operation called getStudents. This is the name of the query and does not need to match backend api's name.
I am also setting that we are receiving a object of StudentRequest as input param.students(body: $studentRequest): The point here to pay attention is that "students" will be response attribute's name: For instance we will receive: data.students.[]
-
@rest: Is trough Rest directive form RestLink that we are able to consume REST APIS.
- type: GraphQL type this service will return.
- path: Path of the API
- method: Rest method
- bodyKey: Since I am sending request body params, that's is the way to attach the method input params to the service.
Attributes Response: In the end I am specifying which attributes of back end service I want to consume.
For our Mutation queries, is follow basically the same format, only difference is that the services do not have a return object.
Hooking application with Apollo and RestLink.
The same way we need to hook a Redux store to an application, we need to hook up Apollo and RestLink.
/apollo.ts
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { RestLink } from 'apollo-link-rest';
const restLink = new RestLink({
uri: 'http://localhost:3000', //process.env.BASE_URL,
headers: {
'Content-Type': 'application/json',
mode: 'cors',
credentials: 'include'
},
});
export const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: restLink,
});
Above I am creating a RestLink object with back end server information, base URI and headers configuration. After that, I am able to create an ApolloClient object setting to use objects in cache memory.
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from '../apollo';
ReactDOM.render(
<ApolloProvider client={apolloClient}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
reportWebVitals();
With the ApolloClient provider set I can hook up into the client-side application.
Consuming APIS with Hooks.
Now is time to execute the call of the apis and manage the return.
In the component below, I have two calls, through useQuery and useMutation. I have shortened the query to make it clear but you can still see the whole code on its Git Repository.
src/components/studentForm/studentForm.ts
import React, { useEffect, useState } from "react";
import StudentModel from "@models/studentModel";
import { useMutation, useQuery } from "@apollo/client";
import { IGetStudentsData, GET_STUDENTS_QUERY, INSERT_STUDENT_GQL } from "@graphql/studentGraph";
import { get } from 'lodash';
export default function StudentForm(props) {
const { loading, error, data } = useQuery<IGetStudentsData>(
GET_STUDENTS_QUERY,
{
variables: { studentRequest: {} }
}
);
const [ insertStudentMutation ] = useMutation<StudentModel>(
INSERT_STUDENT_GQL,
{
refetchQueries: [GET_STUDENTS_QUERY, 'getStudents'],
}
);
useEffect(() => {
if (data) {
const students: StudentModel[] = get(data, 'students', []);
setTotalStudents(students.length);
}
}, [data]);
const insertStudentAsync = () => {
const request: StudentModel = {
firstName,
lastName,
country,
dateOfBirth,
skills: []
};
insertStudentMutation({
variables: {
input: request,
},
});
}
return (
<Component>{...}
);
}
Understanding the code.
As soon the component is rendered, GET api is called through ApolloAPI Hooks useQuery which will make the REST api call.
Obs: If we use useQuery on the same query in a different object, that does no mean there will be another REST API call. But this second component will retrieve Students data from cache. Data that was saved by the first call of the API. That's because I have mentioned above, I am setting my ApolloClient with inMemoryCache
useQuery returns three variables (loading, error, data) which is pretty straightforward and you can use accordingly your requirement.
Mutation: Mutation hook process is a bit different. First we create useMutation function variable, then in the moment we need, we execute it by passing the request input variables. In my example, the mutation is executed in the method insertStudentAsync called by a click of the button.
The interesting part here is the config "refetchQueries". If you know that your app usually needs to refetch certain queries after a particular mutation, you can include a refetchQueries array in that mutation's options:
The parameters expected are the GraphQL variable, plus the name of the query specified. With that, the result will update the cached stored.
Instead of refetchQueries, you can also update the particular attribute of the cached object by the use of "update" from ApolloClient API.
src/components/studentTable/studentTable.ts
import React, { useEffect, useState } from "react";
import StudentModel from "@models/studentModel";
import { useMutation, useQuery } from "@apollo/client";
import { DELETE_STUDENT_GQL, GET_STUDENTS_QUERY, UPDATE_STUDENT_GQL } from "@graphql/studentGraph";
const Row = (
props: {
student: StudentModel,
handleCheck
}
) => {
const classes = useStyles();
const {student, handleCheck} = props;
const [open, setOpen] = useState(false);
const [openDialog, setOpenDialog] = useState(false);
const [updateStudentMutation] = useMutation<StudentModel>(UPDATE_STUDENT_GQL);
async function saveSkillsAsync(newSkill: string) {
const skills = student.skills;
skills.push(newSkill);
const request: StudentModel = {
_id: student._id,
firstName: student.firstName,
lastName: student.lastName,
country: student.country,
dateOfBirth: student.dateOfBirth,
skills: skills
};
updateStudentMutation({
variables: {input: request},
});
closeSkillsDialog();
}
return (
<React.Fragment>
{...}
</React.Fragment>
);
}
export default function StudentTable(props: {}) {
const [selectedAll, setSelectedAll] = useState(false);
const [studentList, setStudentList] = useState<StudentModel[]>([]);
const { loading, error, data } = useQuery<StudentModel[]>(
GET_STUDENTS_QUERY,
{
variables: { studentRequest: {} }
}
);
const [ deleteStudentMutation ] = useMutation<StudentModel>(
DELETE_STUDENT_GQL,
{
refetchQueries: [GET_STUDENTS_QUERY, 'getStudents'],
}
);
useEffect(() => {
console.log(`loading: ${loading}`);
if (!loading && !error) {
const students = get(data, 'students', []);
if (!isEmpty(students)) {
students.forEach(stu => stu.dateOfBirth = formatDate(stu.dateOfBirth));
setStudentList(students);
}
}
}, [data]);
async function deleteStudentsAsync() {
const filter: string[] = studentList
.filter(s => s.checked === true)
.map(x => x._id || '');
if (!isEmpty(filter)) {
deleteStudentMutation({
variables: {
ids: filter
}
});
}
};
return (
<TableContainer component={Paper}>{...}</TableContainer>
);
}
Above we follow the same logic and have more examples of useQuery and useMutation to attend our CRUD functionalities.
To summarise it, is pretty quick and simple to work between GraphQL and RestAPI with RestLink as tool. The examples used above are simple, you can call an API and then refetch the data after a mutation. But ApolloClient is not only that, it covers way more scenario than this, cache control is one of them. I suggest you also check its official website: ApolloClient.
I hope you have enjoyed this article, let me know your thoughts and don't forget to check the whole code on its git repository: react-graphql-client
See ya.
Top comments (0)