JAVASCRIPT
Parse HTTP Link Headers for API Pagination & Navigation
Extract and interpret navigation links (next, prev, first, last) from an API's HTTP 'Link' header using JavaScript, enabling HATEOAS-driven pagination.
function parseLinkHeader(linkHeader) {
const links = {};
if (linkHeader) {
linkHeader.split(',').forEach(linkPart => {
const parts = linkPart.split(';');
const url = parts[0].replace(/<(.*)>/, '$1').trim();
const rel = parts[1].replace(/rel="(.*)"/, '$1').trim();
links[rel] = url;
});
}
return links;
}
// Example Usage:
async function fetchDataWithLinkHeader(url, customFetch = fetch) {
try {
const response = await customFetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const linkHeader = response.headers.get('Link');
const paginationLinks = parseLinkHeader(linkHeader);
const data = await response.json();
console.log('Fetched Data:', data);
console.log('Parsed Link Header:', paginationLinks);
// Example of using pagination links:
if (paginationLinks.next) {
console.log('Next page URL:', paginationLinks.next);
// You could then fetch the next page:
// fetchDataWithLinkHeader(paginationLinks.next);
}
if (paginationLinks.last) {
console.log('Last page URL:', paginationLinks.last);
}
return { data, paginationLinks };
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
}
// Mock API response with a Link header for demonstration
const mockApiFetchWithLinks = async (url) => {
console.log(`Mock Fetching: ${url}`);
const pageMatch = url.match(/page=(\d+)/);
const currentPage = pageMatch ? parseInt(pageMatch[1]) : 1;
const totalPages = 5;
let linkHeader = '';
if (currentPage < totalPages) {
linkHeader += `<https://api.example.com/items?page=${currentPage + 1}&per_page=10>; rel="next"`;
}
if (currentPage > 1) {
if (linkHeader) linkHeader += ', ';
linkHeader += `<https://api.example.com/items?page=${currentPage - 1}&per_page=10>; rel="prev"`;
}
if (linkHeader) linkHeader += ', ';
linkHeader += `<https://api.example.com/items?page=1&per_page=10>; rel="first", `; // Always show first
linkHeader += `<https://api.example.com/items?page=${totalPages}&per_page=10>; rel="last"`; // Always show last
return {
ok: true,
status: 200,
headers: {
get: (headerName) => (headerName === 'Link' ? linkHeader : null),
},
json: () => Promise.resolve({
page: currentPage,
per_page: 10,
total_pages: totalPages,
items: Array.from({ length: 10 }, (_, i) => `Item ${((currentPage - 1) * 10) + i + 1}`)
}),
};
};
// Example 1: Fetching the first page
fetchDataWithLinkHeader('https://api.example.com/items?page=1&per_page=10', mockApiFetchWithLinks)
.then(({ data, paginationLinks }) => {
console.log('--- Page 1 ---');
console.log('Data (first 2 items):', data.items.slice(0,2), '...');
console.log('Links:', paginationLinks);
})
.then(() => {
// Example 2: Fetching a middle page
console.log('
--- Fetching Page 3 ---');
return fetchDataWithLinkHeader('https://api.example.com/items?page=3&per_page=10', mockApiFetchWithLinks);
})
.then(({ data, paginationLinks }) => {
console.log('Data (first 2 items):', data.items.slice(0,2), '...');
console.log('Links:', paginationLinks);
})
.catch(error => console.error('Overall Error:', error));
How it works: Many RESTful APIs use the HTTP `Link` header to provide discoverable navigation links (e.g., 'next', 'prev', 'first', 'last') for pagination and HATEOAS. This JavaScript snippet provides a utility function, `parseLinkHeader`, to parse this header string into a more usable JavaScript object. It helps frontend applications dynamically build pagination controls without hardcoding URL structures, making them more resilient to API changes and promoting a more truly RESTful client implementation.