Using REST APIs with Fetch API and pagination in JavaScript

In this article we’ll see how to fetch data from a REST API with the Fetch API while also handling result pagination.

The Fetch API has a peculiar feature: the Promise returned by calling fetch() is always set to the resolved state. This means that to understand whether the HTTP request was successful, we must check the boolean ok property of the response and throw an exception if its value is false.

async function getData(url) {
        try {
            const request = await fetch(url);
            if(!request.ok) {
                throw new Error('Request error.');
            }
            return await request.json();
        } catch(err) {
            return null;
        }
}

In this case, we will only have an actual JSON value if the initial request was successful.

We delegate data retrieval with pagination to a specific function:

async function getFacts(page = 1) {
        const list = document.getElementById('facts-list');
        const preloader = document.getElementById('preloader');
        preloader.classList.remove('loaded');
        try {
            const url = `${API_ENDPOINT}?page=${page}`;
            const facts = await getData(url);
            if(!facts) {
                throw new Error('Request error.');
            }
            const data = facts.data;
            let contents = '';

            for(const fact of data) {
                contents += `<li>${fact.fact}</li>`;
            }
            list.innerHTML = contents;
            createPaginationLinks(facts.prev_page_url, facts.next_page_url);
            preloader.classList.add('loaded');
        } catch(err) {
            preloader.classList.add('loaded');
            list.innerHTML = '<li class="error">No data to show.</li>';
        }
}

Pagination data is usually returned as a full URL containing the query string with the page number. In this case prev_page_url and next_page_url will contain the URLs of the previous and next pages, or null if such pages do not exist.

Let’s now add the page navigation buttons:

function createPaginationLinks(previous, next) {
        const pagination = document.getElementById('pagination');
        pagination.innerHTML = '';
        if(previous) {
            const previousBtn = document.createElement('button');
            previousBtn.type = 'button';
            previousBtn.dataset.page = previous;
            previousBtn.innerText = 'Previous';
            previousBtn.id = 'previous';
            previousBtn.className = 'pagination-link';
            pagination.appendChild(previousBtn);
        }
        if(next) {
            const nextBtn = document.createElement('button');
            nextBtn.type = 'button';
            nextBtn.dataset.page = next;
            nextBtn.innerText = 'Next';
            nextBtn.id = 'next';
            nextBtn.className = 'pagination-link';
            pagination.appendChild(nextBtn);
        }
}

Each button has the page data attribute where the pagination URL is stored. To handle clicks on these buttons, we must rely on event delegation since the buttons are dynamically updated in the DOM with each request.

function handlePagination() {
        document.addEventListener('click', evt => {
            if(evt.target.classList.contains('pagination-link')) {
                const button = evt.target;
                const link = button.dataset.page;
                const page = parseInt(link.split('?')[1].replace('page=', ''), 10);
                getFacts(page);
            }
        });
}

Finally, we initialize our code:

document.addEventListener('DOMContentLoaded', () => {
        handlePagination();
        getFacts();
});

Demo

JavaScript Fetch API

Conclusion

At a theoretical level, the only potentially problematic technical detail in using the Fetch API concerns handling HTTP exceptions. The rest of the features are definitely more intuitive for the use we want to make of them.

Back to top