Javascript generator to seamlessly query a paginated api
Today is the first time I actually use generators in javascript (typescript) to do something useful, in this case it was iterating over objects reachable through a REST painated api.
Initially the code to query one page of data from the API looked like this
1
2
3
4
5
6
const getProjects = (userId: number, page = 1): Promise<Response> => {
return axios.get(
`https://example.com/api/v1/user/${userId}/projects`,
{ page },
)
}
To explain that code line by line
- function definition as an arrow function, receives a userId and a page number as arguments. The page number argument has a default of 1. The return type defined says that this function returns a
Promiseof an object with theResponsestructure. A function that returns aPromiseis equivalent to anasync function, and both can beawaited - The only line in this function just uses the network library axios to do a GET request. Axios returns a promise, and that promise is directly returned in this line.
- The URL to be queried, this line uses a template string to interpolate the value of the variable
userIdinside the string - This are the GET request querystring parameters that are going to be sent to the server when doing the request
the structure of the response in typescript is like the following
interface Project {
id: number
name: string
}
interface Response {
total_count: number
per_page: number
data: Array<Project>
}
Note that the data of the projects is inside the data attribute and the total_count and per_page are being returned as data in the response.
Then I created a generator to abstract the pagination out. It iterates page by page on the api and returns (yields) every object inside the array directly. An infamous do {} while () loop is used to avoid repeating code, because it needs to be executed at least once.
1
2
3
4
5
6
7
8
9
10
11
async function *getProjectsPaginatedGenerator(userId: number) => {
let page = 0
let res
do {
page += 1
res = await getProjects(userId, page)
for (const project of res.data) {
yield project
}
} while (res.total_count > page * res.per_page)
}
To explain that code line by line
- async generator definition: this is a generator (because of the star) and its async (it returns a promise on each next() call)
- Initializes the
pagevariable to zero - Initializes the
resvariable in the scope outside the body of thedo..whileloop - Starts inconditionally the loop execution
- Increments the
pageto the next one (first time this will be one) - Calls the API and gets the
Responseobject saved in theresvariable. Thisawaitcall is blocking. It the promise fails (i.e. network error, or server response status code >= 400), it throws an exception and halts execution. - Iterates over all the objects (
Project) inside theResponse.data - It
yields every object separately. Generator magic happens here. - Closes for Projects loop
- Ends while loop when there are no more pages that need to be retrieved.
As a bonus, if throttling is needed, a simple solution can be to use a js sleep function like the following one, and await to it just before ending the do..while loop.
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
An example of the generator in use
const userId = 1
for await (const project of getProjectsPaginatedGenerator(userId)) {
console.log(project)
}
The for..of can iterate throuh a generator items that are being yielded async. The awesomeness here is that the iteration is done directly throuh objects and the pagination is not even visible by the use of the generator.