APIIntegracijeNext.jsRESTGraphQLSigurnostWeb razvoj

Vodič za API integracije: najbolje prakse za 2026.

Adrijan Omičević··15 min čitanja
Share

# Što ćete naučiti#

Ovaj vodič za API integracije pokriva kako integrirati third-party i interne API-je u 2026. bez isporuke nepouzdanog, nesigurnog ili operativno skupog koda. Naučit ćete kriterije za odluku REST vs GraphQL, sigurnu autentikaciju, otpornu obradu grešaka i praktične strategije za rate limiting.

Svi primjeri koriste Next.js API routes (App Router) kako bi tajne ostale na serveru i kako bi se logika integracije centralizirala. Ako ste novi u Next.js, krenite s Uvod u Next.js kako biste razumjeli osnove routinga i izvršavanja na serveru.

# Zašto API integracije padaju (i što prvo popraviti)#

Većina incidenata s integracijama nisu situacije tipa “API je pao”. To su predvidivi problemi uzrokovani nedostatkom zaštitnih ograda: bez timeouta, naivni retry, loša klasifikacija grešaka i curenje tajni prema klijentu.

Konkretni obrasci kvarova koje najčešće viđamo:

  • Bez timeouta → zahtjevi vise dok se server resursi ne iscrpe, raste tail latency.
  • Slijepi retry → “retry oluje” pojačavaju outage; troškovi skaču kad se plaćeni API-ji retryaju bez kontrole.
  • Nekonzistentna obrada grešaka → frontend ne može reagirati; korisnici vide generičke greške; podrška ne može trijažirati.
  • Ignoriranje rate limita → burstovi uzrokuju 429, kaskadne retryje i lošiji UX.
  • Prečaci u authu → tokeni cure u preglednik; kompromitirani ključevi vode do downtimea i financijskog rizika.

🎯 Ključna poruka: Integracije tretirajte kao rad s distribuiranim sustavima: dodajte timeoute, retry, rate limiting i observability prije nego dodate “featuree”.

# Arhitekturni obrasci za integracije u 2026.#

Dobar default u 2026. je “Backend-for-Frontend (BFF)” preko Next.js API ruta (ili server actions, kad je prikladno). Cilj je držati vjerodajnice third-party servisa izvan klijenta i standardizirati ponašanje oko grešaka i rate limita.

Najčešće arhitekture integracije#

PatternNajbolje zaPrednostiNedostaci
Direktno klijent → third-party APIJavne API-je, bez tajni, nizak rizikNajniža latencija, najjednostavnijeOtkiva obrasce korištenja, teško za zaštititi, nekonzistentne greške
Next.js API routes kao BFFVećinu aplikacijaTajne ostaju na serveru, centralizirana logika, konzistentan model grešakaDodatni hop, treba rate limiting i cache
Namjenski integration servisVelike organizacije, mnogo potrošačaJasno vlasništvo, reuse, skalabilnoViše infrastrukture i operativnog overhead-a
Event-driven (webhooks/queues)Asinkroni workflowi, sinkronizacija sustavaOtporno, decoupled, podnosi spikeoveViše pokretnih dijelova, eventual consistency

Ako radite customer-facing web/mobile aplikacije, BFF je obično najbrži način da isporučite sigurno. Ako trebate pomoć u dizajnu integration sloja za web i mobile, pogledajte naše usluge web & mobile development.

# REST vs GraphQL u 2026.: kako odabrati#

Oboje radi. Razlika je u tome kako upravljate oblikom podataka, cacheiranjem i governanceom.

REST: najbolje prakse i kada pobjeđuje#

REST je i dalje najčešći izbor za third-party integracije. Posebno je dobar kada:

  • Imate stabilne resurse i predvidljive obrasce pristupa (npr. /orders/:id).
  • Želite semantiku pogodnu za cache (ETagovi, CDN, HTTP caching).
  • Trebate jednostavnije alate i observability (logovi se čisto mapiraju na endpoint-e).

REST zamke na koje treba paziti:

  • Over-fetching/under-fetching što vodi do više zahtjeva.
  • “Sprawl” verzioniranja (/v1, /v2) kad promjene nisu backward compatible.
  • Nekonzistentni error payload-i među endpoint-ima.

GraphQL: najbolje prakse i kada pobjeđuje#

GraphQL je odličan izbor kada:

  • Više klijenata (web, mobile, partner aplikacije) treba različita polja.
  • Želite jedan endpoint s tipiziranom shemom i snažnim toolingom.
  • Trebate kompoziciju podataka iz više izvora iza jednog API-ja.

GraphQL zamke na koje treba paziti:

  • N+1 upiti bez DataLoader-style batchanja.
  • Zlouporaba upita bez complexity/cost limita.
  • Složenije cacheiranje (obično cacheirate na razini field/entity ili s persisted queries).

⚠️ Upozorenje: Ako odaberete GraphQL, rano uvedite limite dubine/kompleksnosti upita i persisted queries. Neograničeni upiti su čest uzrok produkcijskih incidenata.

Brza matrica odluke#

KriterijRESTGraphQL
Jednostavnost integracijeVisokaSrednja
Pogodnost za HTTP caching/CDNVisokaSrednja
Oblici podataka po mjeri klijentaSrednjaVisoka
Rizik skupih upitaNizakVisok (bez limita)
Zrelost toolinga kod vendoraVrlo visokaVisoka

Praktično pravilo: default na REST za third-party providere; izaberite GraphQL kad vaš proizvod ima više klijenata i vi kontrolirate serversku implementaciju.

# Autentikacija: sigurni obrasci koji prežive produkciju#

Većina modernih API-ja koristi OAuth 2.0 (client credentials ili authorization code) ili potpisane tokene (JWT). Cilj je spriječiti curenje tokena, rotirati vjerodajnice i izbjeći nepotrebne privilegije.

Opcije autentikacije koje ćete viđati u 2026.#

MetodaTipičan slučajGdje spremitiNapomene
API KeyJednostavni vendor API-jiServer env varijableRotirajte; ograničite po IP/referreru ako je podržano
OAuth 2.0 Client CredentialsServer-to-serverServer env varijableDohvaćajte kratkoživuće access tokene; cacheirajte do isteka
OAuth 2.0 Authorization Code (PKCE)Integracije vezane uz korisnikaSigurno spremište sessionaKoristite refresh tokene; obradite revocation
JWT (vi ih izdajete)Vaši vlastiti API-jiHttpOnly kolačići / auth headeriValidirajte signature, issuer, audience, exp

Next.js API ruta: OAuth client credentials s cacheiranjem tokena#

Ovaj primjer dohvaća access token s OAuth servera i cacheira ga u memoriji (dovoljno za jedan Node proces). Za serverless/multi-instance okruženja koristite Redis ili KV store.

TypeScript
// app/api/_lib/oauth.ts
type TokenResponse = { access_token: string; expires_in: number; token_type: string };
 
let cachedToken: { value: string; expiresAt: number } | null = null;
 
export async function getAccessToken() {
  const now = Date.now();
  if (cachedToken && cachedToken.expiresAt > now + 30_000) return cachedToken.value;
 
  const res = await fetch(process.env.OAUTH_TOKEN_URL!, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
      scope: "read:orders",
    }),
  });
 
  if (!res.ok) throw new Error(`token_request_failed:${res.status}`);
 
  const data = (await res.json()) as TokenResponse;
  cachedToken = { value: data.access_token, expiresAt: now + data.expires_in * 1000 };
  return cachedToken.value;
}

💡 Savjet: Preferirajte kratkoživuće access tokene. Ako token procuri, “blast radius” je manji nego kod dugotrajnih ključeva.

Nikad ne izlažite tajne pregledniku#

U Next.js, sve što je prefiksirano s NEXT_PUBLIC_ može završiti u client bundleovima. Third-party ključeve držite server-only i pozivajte third-party API kroz vaše API rute.

# Obrada grešaka: dizajn za retry, brži debug i UX#

Korisnike ne zanima da je “Stripe vratio 502.” Njih zanima da je plaćanje palo i je li sigurno pokušati ponovno. Inženjere zanima brzo utvrditi je li problem vaš bug, vendor incident ili rate limiting.

Produkcijski spremna taksonomija grešaka#

Koristite mali skup konzistentnih klasa grešaka:

KategorijaPrimjeriRetry?Tipični HTTP
Validacijanedostaju parametri, invalid stateNe400 / 422
Authneispravan token, nedostaje scopeNe (dok se ne popravi)401 / 403
Not foundresurs ne postojiNe404
Rate limited429, quota exceededDa (nakon odgode)429
Prolazno (transient)timeouts, 502/503, mrežaDa (backoff)502 / 503 / 504
Nepoznatoneklasificirani kvaroviMožda500

Next.js API ruta: konzistentan error envelope + request ID#

Ova ruta poziva upstream REST API i vraća konzistentan oblik greške. Također prosljeđuje request ID radi sljedivosti.

TypeScript
// app/api/orders/[id]/route.ts
import { NextResponse } from "next/server";
import { getAccessToken } from "../../_lib/oauth";
 
function requestIdFrom(req: Request) {
  return req.headers.get("x-request-id") ?? crypto.randomUUID();
}
 
export async function GET(req: Request, ctx: { params: Promise<{ id: string }> }) {
  const requestId = requestIdFrom(req);
  const { id } = await ctx.params;
 
  if (!id) {
    return NextResponse.json(
      { error: { code: "VALIDATION_ERROR", message: "Missing order id", requestId } },
      { status: 422, headers: { "x-request-id": requestId } }
    );
  }
 
  try {
    const token = await getAccessToken();
 
    const upstream = await fetch(`${process.env.UPSTREAM_API_URL!}/orders/${id}`, {
      headers: { Authorization: `Bearer ${token}`, "x-request-id": requestId },
      cache: "no-store",
      signal: AbortSignal.timeout(8_000),
    });
 
    if (upstream.status === 404) {
      return NextResponse.json(
        { error: { code: "NOT_FOUND", message: "Order not found", requestId } },
        { status: 404, headers: { "x-request-id": requestId } }
      );
    }
 
    if (!upstream.ok) {
      return NextResponse.json(
        {
          error: {
            code: "UPSTREAM_ERROR",
            message: "Upstream API error",
            status: upstream.status,
            requestId,
          },
        },
        { status: 502, headers: { "x-request-id": requestId } }
      );
    }
 
    const data = await upstream.json();
    return NextResponse.json({ data, requestId }, { headers: { "x-request-id": requestId } });
  } catch (err) {
    const message = err instanceof Error ? err.message : "unknown_error";
    const isTimeout = message.includes("timeout") || message.includes("AbortSignal");
 
    return NextResponse.json(
      {
        error: {
          code: isTimeout ? "TIMEOUT" : "INTEGRATION_ERROR",
          message: isTimeout ? "Upstream request timed out" : "Integration failed",
          requestId,
        },
      },
      { status: 504, headers: { "x-request-id": requestId } }
    );
  }
}

Zašto je ovo važno:

  • Frontend može prikazati konkretnije poruke i odlučiti je li “Pokušaj ponovno” prikladno.
  • Podrška može tražiti requestId i brzo pronaći logove.
  • Engineering može razlikovati 404 vs upstream 5xx vs timeout.

# Retry, timeouti i idempotency (napravite ovo ili platite kasnije)#

Retry je potreban za prolazne greške, ali mora biti kontroliran. U 2026. mnogi API-ji se naplaćuju po zahtjevu; nekontrolirani retry može izravno povećati trošak.

Pravila koja rade u produkciji#

  • Uvijek postavite timeout. Čest baseline je 5–10 sekundi za upstream pozive, niže za UX-kritične putanje.
  • Retryajte samo idempotentne operacije (GET/HEAD sigurno; POST samo s idempotency ključevima).
  • Koristite eksponencijalni backoff + jitter kako biste izbjegli sinkronizirane retry oluje.
  • Ograničite retry na 2–3 pokušaja za user-facing zahtjeve.

Next.js helper: fetch s retryjem + backoffom#

Retry neka bude kratak i eksplicitan. Ovaj helper retrya samo na mrežne greške, 429 i 5xx.

TypeScript
// app/api/_lib/fetchWithRetry.ts
export async function fetchWithRetry(
  url: string,
  init: RequestInit,
  opts: { retries?: number; timeoutMs?: number } = {}
) {
  const retries = opts.retries ?? 2;
  const timeoutMs = opts.timeoutMs ?? 8000;
 
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const res = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
      if (res.ok) return res;
 
      const retryable = res.status === 429 || (res.status >= 500 && res.status <= 599);
      if (!retryable || attempt === retries) return res;
    } catch (e) {
      if (attempt === retries) throw e;
    }
 
    const backoff = Math.round((200 * 2 ** attempt) * (0.7 + Math.random() * 0.6));
    await new Promise((r) => setTimeout(r, backoff));
  }
 
  throw new Error("unreachable");
}

Idempotency ključevi za siguran retry POST-a#

Ako provider podržava idempotency (Stripe-style), generirajte ključ po logičkoj radnji i spremite ga uz order/payment zapis. Ako klijent retrya (refresh, double-click), nećete dvaput naplatiti ili dvaput kreirati resurse.

# Rate limiting: zaštitite aplikaciju i poštujte providere#

Rate limiting ima dvije strane:

  1. 1
    Inbound: zaštitite svoje Next.js API rute od zlouporabe i slučajnih burstova.
  2. 2
    Outbound: spriječite da “zatrpate” third-party API i dobijete 429.

U 2026. veliki provideri obično nameću per-minute kvote i burst limite. Mnogi također vraćaju Retry-After headere na 429.

Implementirajte inbound rate limiting u Next.js API rutama (jednostavan baseline)#

Za produkciju koristite Redis/KV za dijeljene brojače. Ovaj in-memory primjer je koristan za brzu zaštitu u jednoj instanci.

TypeScript
// app/api/_lib/rateLimit.ts
const buckets = new Map<string, { count: number; resetAt: number }>();
 
export function rateLimit(key: string, limit: number, windowMs: number) {
  const now = Date.now();
  const bucket = buckets.get(key);
 
  if (!bucket || bucket.resetAt <= now) {
    buckets.set(key, { count: 1, resetAt: now + windowMs });
    return { ok: true, remaining: limit - 1, resetAt: now + windowMs };
  }
 
  if (bucket.count >= limit) return { ok: false, remaining: 0, resetAt: bucket.resetAt };
 
  bucket.count += 1;
  return { ok: true, remaining: limit - bucket.count, resetAt: bucket.resetAt };
}

Upotreba u ruti:

TypeScript
// app/api/public/search/route.ts
import { NextResponse } from "next/server";
import { rateLimit } from "../../_lib/rateLimit";
 
export async function GET(req: Request) {
  const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
  const rl = rateLimit(`search:${ip}`, 60, 60_000);
 
  if (!rl.ok) {
    const retryAfter = Math.max(1, Math.ceil((rl.resetAt - Date.now()) / 1000));
    return NextResponse.json(
      { error: { code: "RATE_LIMITED", message: "Too many requests" } },
      { status: 429, headers: { "retry-after": String(retryAfter) } }
    );
  }
 
  return NextResponse.json({ data: { ok: true }, rateLimit: { remaining: rl.remaining } });
}

Outbound throttling: ne dopustite da vaša aplikacija DDOS-a vendora#

Ako zovete providera s poznatim limitom (npr. 10 req/s), nametnite queue ili token bucket na svojoj strani—posebno za batch jobove i webhooks. Ako već koristite n8n za automatizaciju, izrada throttled workflowa često je brža nego ručno “rolanje” queuea.

ℹ️ Napomena: U serverless deploymentima s više instanci, outbound throttling mora biti centraliziran (Redis/KV/queue). Throttling po instanci neće spriječiti prekoračenje agregiranih limita.

# Primjer REST integracije: Next.js API ruta kao stabilna fasada#

Česta najbolja praksa je izložiti stabilan interni endpoint (vaš ugovor) i prilagodbe na promjene third-party providera raditi iza njega. To smanjuje churn u frontendu kad vendor promijeni polja ili formate grešaka.

Primjer: normalizirajte upstream response i sigurno cacheirajte#

Ako se podaci rijetko mijenjaju, dodajte cache na BFF razini. Za user-specific resurse izbjegavajte zajedničke cacheve osim ako ih pravilno ključate.

TypeScript
// app/api/catalog/route.ts
import { NextResponse } from "next/server";
import { fetchWithRetry } from "../_lib/fetchWithRetry";
 
export async function GET() {
  const res = await fetchWithRetry(`${process.env.UPSTREAM_API_URL!}/catalog`, {
    headers: { "accept": "application/json", "x-api-key": process.env.UPSTREAM_API_KEY! },
    next: { revalidate: 300 },
  });
 
  if (!res.ok) {
    return NextResponse.json(
      { error: { code: "UPSTREAM_ERROR", message: "Catalog unavailable" } },
      { status: 502 }
    );
  }
 
  const upstream = await res.json();
  const items = (upstream.items ?? []).map((i: any) => ({
    id: String(i.id),
    title: String(i.name),
    priceCents: Number(i.price_cents),
  }));
 
  return NextResponse.json({ data: { items } });
}

Zašto je ovo važno:

  • Vi kontrolirate response contract (id, title, priceCents) čak i ako provider promijeni nazive polja.
  • revalidate: 300 može smanjiti broj upstream poziva i do 95%+ za često posjećene katalog stranice, ovisno o prometnim obrascima.

# Primjer GraphQL integracije: persisted queries i sigurniji fetching#

Ako konzumirate GraphQL API, izbjegavajte slanje proizvoljnih upita s klijenta. Preferirajte server-side integraciju (BFF) i koristite persisted queries gdje je podržano.

Primjer: server-side GraphQL POST s varijablama#

TypeScript
// app/api/profile/route.ts
import { NextResponse } from "next/server";
import { getAccessToken } from "../_lib/oauth";
 
const query = `
  query Profile($id: ID!) {
    user(id: $id) { id name email }
  }
`;
 
export async function GET(req: Request) {
  const userId = new URL(req.url).searchParams.get("id");
  if (!userId) {
    return NextResponse.json(
      { error: { code: "VALIDATION_ERROR", message: "Missing id" } },
      { status: 422 }
    );
  }
 
  const token = await getAccessToken();
  const res = await fetch(process.env.GRAPHQL_URL!, {
    method: "POST",
    headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
    body: JSON.stringify({ query, variables: { id: userId } }),
    signal: AbortSignal.timeout(8_000),
    cache: "no-store",
  });
 
  if (!res.ok) {
    return NextResponse.json(
      { error: { code: "UPSTREAM_ERROR", message: "GraphQL request failed" } },
      { status: 502 }
    );
  }
 
  const payload = await res.json();
  if (payload.errors?.length) {
    return NextResponse.json(
      { error: { code: "UPSTREAM_GRAPHQL_ERROR", message: payload.errors[0].message } },
      { status: 502 }
    );
  }
 
  return NextResponse.json({ data: payload.data.user });
}

Produkcijski savjeti za GraphQL potrošače:

  • Koristite allowlisted/persisted queries kad god možete.
  • Validirajte shape response-a; ne pretpostavljajte da data postoji.
  • Pratite latenciju i error rate po operation nameu.

# Observability: logovi, metrike i tracing koji stvarno pomažu#

Ne treba vam savršen tracing da biste povećali pouzdanost. Trebaju vam konzistentni metapodaci i nekoliko ključnih metrika.

Minimalna observability checklista#

SignalŠto uhvatitiZašto je važno
Request IDx-request-id proslijeđen end-to-endBrza korelacija kroz servise
Timingukupna latencija + upstream latencijaPrepoznavanje uskih grla i regresija
Error kodovivaši stabilni kodovi (RATE_LIMITED, TIMEOUT)Praćenje stvarnih failure modova
Upstream statusraspodjela 2xx/4xx/5xxVendor probleme vidite odmah
Rate limit headeriremaining, resetPredviđanje throttlinga prije incidenta

Praktično pravilo logiranja: logirajte metapodatke, ne osjetljive payloadove. Ako baš morate logirati isječke payloada, redaktirajte PII i tajne.

# Česte zamke (izdanje 2026.)#

  1. 1
    Korištenje fetch() bez timeouta — produkcijski “hangovi” postaju “random sporost.” Uvijek koristite AbortSignal.timeout.
  2. 2
    Retry POST-a bez idempotencyja — stvara duplikate i financijske incidente. Koristite idempotency ključeve ili nemojte retryati.
  3. 3
    Vraćanje raw upstream grešaka frontendu — otkriva vendor detalje i prisiljava promjene u UI-ju. Normalizirajte greške.
  4. 4
    Ignoriranje semantike 429 — tretirajte 429 kao first-class response uz podršku za Retry-After.
  5. 5
    Spremanje API ključeva u klijenta — čak i “privremeni” prečaci završe isporučeni. Tajne držite server-side kroz API rute.
  6. 6
    Bez contract testova za integracije — provideri mijenjaju stvari. Dodajte osnovne schema/contract asercije u CI za kritične endpoint-e.

# Ključne poruke#

  • Držite third-party vjerodajnice na serveru koristeći Next.js API rute kao BFF i izložite stabilan interni contract.
  • Birajte REST radi jednostavnosti i cacheiranja; birajte GraphQL kad trebate fleksibilne oblike podataka—i tada uvedite limite kompleksnosti i sigurnije obrasce upita.
  • Implementirajte timeoute + retry s backoffom i retryajte samo retryable kvarove; dodajte idempotency ključeve za siguran retry POST-a.
  • Normalizirajte greške u konzistentan envelope sa stabilnim error kodovima i prosljeđujte request ID-jeve za brži debugging.
  • Rate limiting tretirajte kao zahtjev proizvoda: nametnite inbound kvote i outbound throttling te obradite 429 uz Retry-After.

# Zaključak#

Pouzdana API integracija u 2026. manje je stvar prvog uspješnog zahtjeva, a više onoga što se događa pod opterećenjem, tijekom vendor incidenata i kad promet naglo skoči. Ako implementirate BFF u Next.js, standardizirate auth, timeoute, retry, error envelope i rate limiting, isporučit ćete integracije koje ostaju stabilne kako proizvod raste.

Ako želite da Samioda dizajnira i implementira vaš integration sloj (web + mobile, uključujući automation workflowe), javite se putem web & mobile development. Za Next.js osnove prije nego krenete, koristite Uvod u Next.js.

FAQ

Share
A
Adrijan OmičevićSamioda Team
All articles →

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.