Learn Nuxt - API Integration & Data Protection
Series/Learn Nuxt/Episode 13
Episode 13 of 24

Learn Nuxt - API Integration & Data Protection

This episode covers external API integration in Nuxt: calling third-party services from the server, protecting requests with auth headers, storing secrets safely via runtime config, and applying rate limiting and resilient error handling.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Real applications rarely work alone — they call payment gateways, email services, or weather APIs. Episode 13 covers how to integrate external APIs correctly in Nuxt: where to call them from, how requests are secured, where secrets are stored, and how to handle failures.

The most important rule of this episode: external API calls that need secrets must happen on the server, not in the browser. Client code can be read by anyone, so API keys and tokens may only live on the server side.

Using External APIs in Nuxt

Calling from Server Routes

The most appropriate place for external calls is server/api. Let's create an endpoint that connects to a payment gateway:

JSserver/api/pembayaran/index.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
 
  const hasil = await $fetch("https://api.payment.dev/v1/charge", {
    method: "POST",
    body,
    headers: {
      Authorization: `Bearer ${useRuntimeConfig().apiKeyPembayaran}`,
    },
  })
 
  return hasil
})

$fetch in a server route carries the Authorization header taken from runtime config. The browser never sees this API key because the entire call happens on the server.

A Proxy for CORS Problems

Often the issue isn't security but CORS — the browser blocks cross-origin calls. Using a server route as a proxy solves two problems at once: CORS and secret protection.

Protecting API Requests and Auth Headers

Always Include the Token

Every request to an external API that requires authorization must carry a token. Store the token in runtime config and never hardcode it:

JSRequest dengan auth header
export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig()
 
  const res = await $fetch("https://api.eksternal.dev/v2/produk", {
    headers: {
      "X-Api-Key": config.apiKeyEksternal,
    },
  })
 
  return res
})

config.apiKeyEksternal is read from NUXT_API_KEY_EKSTERNAL in the environment. Custom headers like X-Api-Key are a common pattern used by many third-party APIs.

Endpoints That Don't Need a Token

Some public APIs indeed don't need a token, but you should still restrict who can call them. If an API is paid or has quotas, don't let anyone call it without control.

Securely Handling Sensitive Data and Secrets

Secrets Only on the Server Side

Classifying data is crucial. What may go in runtimeConfig.public is only public data like base URLs. API keys, tokens, and passwords only go in the private runtimeConfig:

JSMemisahkan secret
export default defineNuxtConfig({
  runtimeConfig: {
    stripeSecret: process.env.STRIPE_SECRET_KEY,
    public: {
      stripePublishable: process.env.STRIPE_PUBLISHABLE_KEY,
    },
  },
})

stripeSecret is never sent to the client, while stripePublishable is safe to use in the frontend — for example to display a card form.

Don't Log Secrets

A common mistake: console.log(config.stripeSecret) while debugging, then leaking it into server logs that others can read. Make a simple rule: never display secrets in logs, error messages, or API responses.

Rate Limiting

Protecting Endpoints from Abuse

Rate limiting limits the number of requests per user. Nitro provides storage well suited to this pattern:

JSRate limiting berbasis IP
export default defineEventHandler(async (event) => {
  const ip = getRequestIP(event, { xForwardedFor: true })
  const kunci = `rate:${ip}`
  const storage = useStorage("cache")
  const jumlah = (await storage.getItem(kunci)) || 0
 
  if (jumlah >= 100) {
    throw createError({
      statusCode: 429,
      message: "Terlalu banyak request",
    })
  }
 
  await storage.setItem(kunci, jumlah + 1, { ttl: 3600 })
  return { ok: true }
})

The pattern above counts requests per IP using Nitro storage with a one-hour TTL. Status 429 is returned when the limit is exceeded — protecting the endpoint from brute force and abuse.

Reasonable Limits

Adjust limits to the nature of the endpoint: public pages can be more lenient, login endpoints should be strict. Combine with exponential backoff on the client side for a still-good experience.

Error Handling

Catching External Failures

Third-party APIs can fail, be slow, or change formats. Server routes must catch errors and return clear messages:

JSMenangkap error eksternal
export default defineEventHandler(async () => {
  try {
    const res = await $fetch("https://api.eksternal.dev/v1/status")
    return res
  } catch (e) {
    throw createError({
      statusCode: 503,
      message: "Layanan eksternal sedang tidak tersedia",
    })
  }
})

createError({ statusCode: 503, ... }) communicates the failure to the client with the right status. Never leak internal details like stack traces to users.

Retry with Limits

For transient failures, add retries with increasing delays:

JSRetry dengan jeda meningkat
for (let i = 1; i <= 3; i++) {
  try {
    return await $fetch("https://api.eksternal.dev/v1/resi")
  } catch (e) {
    if (i === 3) throw e
    await new Promise((r) => setTimeout(r, i * 500))
  }
}

The retry pattern above tries up to three times with graduated delays. Combine it with a timeout so hanging requests don't consume server resources.

Conclusion

Episode 13 secures your external integrations: third-party API calls happen from the server, requests are protected with auth headers, secrets are stored only on the server side via runtime config, rate limiting protects endpoints from abuse, and error handling makes the system resilient to failures.

Key takeaways:

  • Call external APIs from server routes, not from the browser.
  • A server route as a proxy solves CORS while protecting secrets.
  • API keys and tokens go only in the private runtimeConfig, never in public.
  • Don't display secrets in logs, errors, or responses.
  • Use Nitro storage for rate limiting to protect endpoints.
  • Catch external errors and return clear messages without internal details.

In the next episode, episode 14, we will discuss caching and performance — caching data with Nitro and browser cache, static generation and ISR, page load optimization and asset delivery, and monitoring build performance. Your store will start running fast.

Learn Nuxt - API Integration & Data Protection | Learn Nuxt