This episode covers integrating external data: fetching APIs in loaders and actions, working with REST and GraphQL backends, secure auth token and request handling, and good rate limiting and error fallbacks.

Real applications rarely live alone. Search uses external services, payments use third-party providers, and recommendations come from AI models. Episode 13 covers how Remix talks to the outside world securely and efficiently.
The biggest advantage: because loaders and actions run on the server, external API calls are invisible to the client. Tokens and keys stay hidden, CORS is not a problem, and data can be processed before it's sent to the browser. This is another reason Remix's server-first pattern is so practical.
Episode 13 covers fetching in loaders and actions, REST and GraphQL integration, token handling, and rate limiting with error fallbacks.
A loader can call an external API with fetch, process the response, and send clean results to the component:
export async function loader() {
const res = await fetch("https://api.contoh.dev/posts", {
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
});
if (!res.ok) {
throw new Response("Gagal memuat data", { status: 502 });
}
const posts = await res.json();
return { posts };
}The token is never visible to the client because the fetch happens on the server. CORS is irrelevant here — you're free to call any domain from the server.
A loader receives a request whose URL can be read for page parameters, search terms, or filters. new URL(request.url) gives access to searchParams, which are then forwarded to the external API. This pattern keeps one source of truth: the URL.
REST APIs generally return more data than you need. Transforming in the loader — picking fields, renaming them, adding supporting data — keeps components simple and the client payload as small as possible.
export async function loader() {
const res = await fetch("https://api.contoh.dev/posts?limit=10");
const data = await res.json();
const ringkas = data.posts.map((p) => ({
id: p.id,
judul: p.title,
penulis: p.author.username,
}));
return { posts: ringkas };
}Transforming in the loader means the client doesn't have to rewrite raw data. The structure sent to the browser matches the UI's needs, not the API's needs.
GraphQL lets you request exactly the fields you need in a single request. In the loader, just send the query and token:
export async function loader() {
const query = "{ posts { id title } }";
const res = await fetch("https://api.contoh.dev/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.GRAPHQL_TOKEN}`,
},
body: JSON.stringify({ query }),
});
return { posts: (await res.json()).data.posts };
}GraphQL reduces over-fetching, but still run it on the server to keep tokens safe. For complex queries, libraries like urql or Apollo can be used, but plain fetch is often enough for a route's needs.
External auth tokens are stored in environment variables and only read in loaders and actions. Never expose them to the client. If a token needs to be used on the client for some reason, create a resource route that acts as a proxy — the client calls your endpoint, not the external API.
External requests can fail or be slow. Add a timeout with AbortSignal and consider retrying for transient errors. A healthy pattern: fail fast with a clear message rather than hang without any word.
External APIs have quotas. Avoid repeated calls for data that rarely changes by reusing the caching from episode 9. Also consider queues for heavy work that doesn't have to be synchronous.
When an external API goes down, your application shouldn't go down with it. A common pattern: provide backup data (for example, the last version from the database), or show a clear empty state with a message users can understand.
export async function loader() {
try {
const res = await fetch("https://api.contoh.dev/posts");
if (!res.ok) throw new Error("bad status");
return { posts: await res.json() };
} catch {
return { posts: [], sumber: "cadangan" };
}
}Try/catch in the loader turns an external failure into a controlled response. The UI still renders, and users are informed through the source field.
Episode 13 opens your application to the world: external fetching in loaders and actions, REST and GraphQL integration with data transformation, tokens that only live on the server, and rate limiting with error fallbacks that keep the application breathing when other services fall over.
The key takeaways:
In the next episode, episode 14, we'll discuss network performance and caching — HTTP caching strategies for Remix, CDN and edge cache integration, prefetching data and links, and optimizing the first load and subsequent navigation. External data comes in; now let's make sure it travels as fast as possible.