civil-and-structural-engineering
Using Javascript to Fetch and Display Data from Graphql Apis
Table of Contents
GraphQL APIs have transformed how modern web applications fetch and manage data, offering remarkable flexibility and efficiency. When paired with JavaScript, developers can build dynamic, responsive sites that pull only the data they need from a single endpoint. This expanded guide dives deep into fetching and displaying data from GraphQL APIs using vanilla JavaScript, covering best practices, error handling, and real-world examples to equip you with production-ready skills.
What Makes GraphQL Different?
GraphQL is a query language for APIs defined by a schema that describes available data and operations. Unlike REST, which requires multiple endpoints for different resources (/users, /users/:id, /posts), GraphQL uses a single endpoint (commonly /graphql). Clients send a POST request containing a query that specifies exactly which fields they want. This eliminates over-fetching (getting too much data) and under-fetching (needing multiple requests) — two common pain points in RESTful integrations. JavaScript's fetch API and modern asynchronous patterns make this interaction seamless.
Setting Up a Basic GraphQL Query with Fetch
To fetch data from a GraphQL API, you send a POST request with a JSON body that contains a query string. The server processes the query and returns a JSON object with a data field (and possibly an errors field). Here’s a minimal example running against a public GraphQL API (like the SpaceX or Pokemon API):
const query = `
query {
launches(limit: 5) {
mission_name
launch_date_utc
}
}
`;
fetch('https://api.spacexdata.com/v4/launches/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
})
.then(response => response.json())
.then(data => {
console.log(data.data.launches);
})
.catch(error => console.error('Network error:', error));
The Content-Type header is mandatory for the server to interpret the body correctly. fetch returns a Promise that resolves to the Response object; calling .json() extracts the JSON body. Notice how the query is wrapped in backticks for template literals, enabling multi-line strings.
Switching to Async/Await for Cleaner Code
Chaining .then() works, but async/await makes the code read more synchronously and simplifies error handling with try/catch. This pattern is especially valuable when you need to perform multiple steps after fetching data.
async function fetchLaunches() {
const query = `
query {
launches(limit: 5) {
mission_name
launch_date_utc
}
}
`;
try {
const response = await fetch('https://api.spacexdata.com/v4/launches/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const result = await response.json();
if (result.errors) {
console.error('GraphQL errors:', result.errors);
} else {
console.log(result.data.launches);
}
} catch (error) {
console.error('Fetch failed:', error);
}
}
fetchLaunches();
Note the addition of checking result.errors. GraphQL APIs return an HTTP 200 status even when queries have logical errors (like requesting a non-existent field). The actual errors appear inside the errors array, so you must inspect that separately from network issues.
Using Variables for Dynamic Queries
Hard-coding values in queries is not scalable. GraphQL supports variables that you pass alongside the query string. Variables are defined in the query signature and then referenced inside the query. The variables object is sent as part of the request body under the variables key.
const query = `
query GetLaunch($id: ID!) {
launch(id: $id) {
mission_name
launch_year
details
}
}
`;
const variables = { id: '13' }; // Example: launch ID
async function fetchLaunchById(id) {
try {
const response = await fetch('https://api.spacexdata.com/v4/launches/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: { id } })
});
const result = await response.json();
return result.data.launch;
} catch (error) {
console.error('Failed to fetch launch:', error);
}
}
Using variables makes your code reusable, easier to read, and helps prevent injection attacks (though GraphQL queries are often server-side parsed, it’s still a good practice).
Displaying Data in the DOM
Fetching data is only half the work. To show it on a page, you need to manipulate the Document Object Model (DOM). The safest and most performant approach is to create elements using document.createElement and then append them, rather than using innerHTML (which can cause XSS vulnerabilities if data isn't sanitized).
Rendering a List of Items
Suppose you have an API that returns a list of Pokemon. You want to display each Pokemon's name and type as a card inside a container.
const pokemonContainer = document.getElementById('pokemon-cards');
function renderPokemon(pokemonList) {
pokemonList.forEach(pokemon => {
const card = document.createElement('div');
card.className = 'card';
const name = document.createElement('h3');
name.textContent = pokemon.name;
const types = document.createElement('p');
types.textContent = `Types: ${pokemon.types.map(t => t.type.name).join(', ')}`;
card.appendChild(name);
card.appendChild(types);
pokemonContainer.appendChild(card);
});
}
async function loadPokemon() {
const query = `
query GetPokemon {
pokemon_v2_pokemon(limit: 10) {
name
pokemon_v2_pokemontypes {
pokemon_v2_type {
name
}
}
}
}
`;
try {
const response = await fetch('https://beta.pokeapi.co/graphql/v1beta', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const result = await response.json();
const pokemonList = result.data.pokemon_v2_pokemon;
renderPokemon(pokemonList);
} catch (error) {
console.error('Error fetching Pokemon:', error);
}
}
loadPokemon();
Rendering a Table
For tabular data, such as SpaceX launches, a table is more appropriate. Create a <table> element, add a header row, and fill rows with fetched data.
const table = document.getElementById('launch-table');
const tbody = table.querySelector('tbody') || table.createTBody();
function renderLaunches(launches) {
launches.forEach(launch => {
const row = document.createElement('tr');
const nameCell = document.createElement('td');
nameCell.textContent = launch.mission_name;
const dateCell = document.createElement('td');
dateCell.textContent = new Date(launch.launch_date_utc).toLocaleDateString();
row.appendChild(nameCell);
row.appendChild(dateCell);
tbody.appendChild(row);
});
}
Error Handling Best Practices
Real-world applications must handle various failure scenarios gracefully. Distinguish between network errors (no internet, server down) and GraphQL errors (invalid query, missing permissions).
- Network errors – The
fetchPromise rejects if the request fails to reach the server. Wrap calls intry/catchand display a user-friendly message. - HTTP errors (4xx, 5xx) – By default,
fetchdoes not reject on non-2xx status codes. Always checkresponse.okorresponse.statusafter callingfetch. - GraphQL errors – The server returns a 200 status with an
errorsarray. Check forresult.errorsbefore using thedatafield.
Here’s a more robust fetch wrapper that handles all three scenarios:
async function graphqlRequest(url, query, variables = {}) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.errors) {
throw new Error(result.errors.map(e => e.message).join('; '));
}
return result.data;
} catch (error) {
console.error('GraphQL request failed:', error.message);
throw error; // re-throw so caller can handle UI update
}
}
Use this wrapper across your application to centralize error logic.
Adding Authentication Headers
Many GraphQL APIs require an HTTP header like Authorization: Bearer <token>. You need to set the Authorization header inside the headers object. For example, when working with GitHub's GraphQL API (https://api.github.com/graphql):
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_GITHUB_TOKEN'
};
fetch('https://api.github.com/graphql', {
method: 'POST',
headers,
body: JSON.stringify({ query })
});
Never hard-code tokens in client-side code (unless it's a public token with limited scope). In production, use environment variables or fetch a token from a secure backend endpoint.
Working with Pagination
GraphQL APIs often implement pagination using connections or simple offset/limit. The example above used limit: 10 to fetch only ten Pokemon. For real applications, you'll need to handle "load more" or infinite scroll. One common pattern is to use a cursor argument (for connection-based pagination) or offset and limit.
Here’s how you could fetch the next page of launches:
async function fetchMoreLaunches(offset = 0, limit = 5) {
const query = `
query GetLaunches($offset: Int!, $limit: Int!) {
launches(offset: $offset, limit: $limit) {
id
mission_name
}
}
`;
const data = await graphqlRequest('https://api.spacexdata.com/v4/launches/graphql', query, { offset, limit });
return data.launches;
}
Then in your application, keep a currentOffset variable and increment it each time the user clicks "Load more".
Performance Considerations
GraphQL itself reduces over-fetching, but you can further improve performance on the client side:
- Cache responses – Use a simple in-memory cache keyed by the query string + variables. Avoid re-fetching the same data if it’s still fresh.
- Debounce input – If you’re making queries based on user typing (e.g., search), debounce to reduce network calls.
- Batch requests – Some GraphQL libraries (like Apollo Client) support batching multiple queries into one request. With vanilla JS, you can chain queries or use a single query with multiple fields.
Remember that GraphQL requests are typically POST, so they are not cached by the browser’s HTTP cache by default. Use explicit caching strategies or service workers for offline support.
Real-World Example: Building a User Dashboard
Let’s put everything together to build a small user dashboard. Assume we have a GraphQL endpoint that returns users with name, email, and active status. We’ll display them as cards with an error boundary.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Dashboard</title>
</head>
<body>
<div id="user-list"></div>
<div id="error-message" style="color: red;"></div>
<script>
const userList = document.getElementById('user-list');
const errorDiv = document.getElementById('error-message');
function renderUser(user) {
const card = document.createElement('div');
card.className = 'user-card';
const name = document.createElement('h2');
name.textContent = user.name;
const email = document.createElement('p');
email.textContent = user.email;
const status = document.createElement('span');
status.textContent = user.active ? 'Active' : 'Inactive';
status.style.color = user.active ? 'green' : 'gray';
card.appendChild(name);
card.appendChild(email);
card.appendChild(status);
return card;
}
async function loadUsers() {
const query = `
query GetUsers {
users {
id
name
email
active
}
}
`;
try {
const data = await graphqlRequest('https://api.example.com/graphql', query);
data.users.forEach(user => {
userList.appendChild(renderUser(user));
});
} catch (error) {
errorDiv.textContent = 'Failed to load users. Please try again later.';
}
}
// Assume graphqlRequest defined elsewhere
loadUsers();
</script>
</body>
</html>
External Resources for Deeper Learning
To master GraphQL and JavaScript fetching, explore these authoritative sources:
- GraphQL official learning site – Understand queries, mutations, subscriptions, and schema design.
- MDN Fetch API documentation – Comprehensive reference for the
fetchmethod. - MDN Async/Await guide – Understand promises and async/await in depth.
Conclusion
Fetching and displaying data from GraphQL APIs using JavaScript is a straightforward but powerful pattern. By mastering fetch with async/await, handling variables and errors properly, and rendering content safely in the DOM, you can build interactive applications that are efficient and maintainable. Experiment with different public GraphQL APIs (like GitHub, SpaceX, or Pokemon) to solidify your skills. The combination of GraphQL’s precise data fetching and JavaScript’s dynamic DOM manipulation creates a robust foundation for modern web development.