DEV Community

ApogeoAPI
ApogeoAPI

Posted on • Originally published at apogeoapi.com

SvelteKit Localized Pricing with ApogeoAPI in 30 Lines (Server Hooks)

SvelteKit's server hooks run on every request before the page is rendered. Combined with ApogeoAPI, you can detect the visitor's country and look up the live exchange rate, then pass everything down to every +page.svelte as typed locals.

The hook

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { APOGEOAPI_KEY } from '$env/static/private';
const APOGEO = 'https://api.apogeoapi.com/v1/api/geo';
export const handle: Handle = async ({ event, resolve }) => {
const ip =
event.request.headers.get('x-forwarded-for')?.split(',')[0] ??
event.getClientAddress();
if (ip && APOGEOAPI_KEY) {
try {
const res = await fetch(`${APOGEO}/ip/${ip}`, {
headers: { 'X-API-Key': APOGEOAPI_KEY },
});
if (res.ok) {
const data = await res.json();
event.locals.geo = {
country: data.country?.iso2,
currency: data.country?.currency,
rate: data.country?.currencyRate, // USD → local
};
}
} catch {
// fail-open
}
}
return resolve(event);
};
Enter fullscreen mode Exit fullscreen mode

Type the locals

// src/app.d.ts
declare global {
namespace App {
interface Locals {
geo?: {
country?: string;
currency?: string;
rate?: number;
};
}
}
}
export {};
Enter fullscreen mode Exit fullscreen mode

Use in a +page.server.ts

// src/routes/pricing/+page.server.ts
import type { PageServerLoad } from './$types';
const PLANS = [
{ name: 'Basic', usd: 19 },
{ name: 'Starter', usd: 29 },
{ name: 'Professional', usd: 79 },
];
export const load: PageServerLoad = async ({ locals }) => {
const { currency, rate } = locals.geo ?? {};
const plans = PLANS.map((p) => ({
...p,
localPrice:
currency && rate && currency !== 'USD'
? new Intl.NumberFormat(undefined, {
style: 'currency',
currency,
maximumFractionDigits: 0,
}).format(p.usd * rate)
: null,
}));
return { plans, currency };
};
Enter fullscreen mode Exit fullscreen mode

Render in +page.svelte


import type { PageData } from './$types';
export let data: PageData;

- **{plan.name}**: ${plan.usd} USD {#if plan.localPrice}{plan.localPrice} {/if}

Enter fullscreen mode Exit fullscreen mode

Caching the FX lookup

The hook above hits ApogeoAPI once per request — great for testing, expensive at scale. Use SvelteKit's fetch caching options:

const res = await event.fetch(`${APOGEO}/ip/${ip}`, {
headers: { 'X-API-Key': APOGEOAPI_KEY },
// SvelteKit-specific: cache by URL for 1h on the server
cache: 'force-cache',
});
Enter fullscreen mode Exit fullscreen mode

For more granular control (e.g. invalidate hourly), wrap the call in a small in-memory Map with TTL. ~15 lines and you're done.

Common pitfalls

  • Static adapter. If you're using @sveltejs/adapter-static, hooks don't run — the site is fully prerendered. Switch to adapter-vercel, adapter-cloudflare, or adapter-node if you need geo personalization.
  • Localhost dev. event.getClientAddress() returns 127.0.0.1 which ApogeoAPI rejects. Hardcode a real IP behind dev guard for local testing.
  • Server-only env. $env/static/private only works in server contexts (+page.server.ts, +layout.server.ts, hooks, endpoints). Never reference APOGEOAPI_KEY in +page.svelte — that exposes it to the browser.

Free API key + 1,000 calls/month at apogeoapi.com.


Originally published at https://apogeoapi.com/blog/sveltekit-localized-pricing. Try ApogeoAPI free at apogeoapi.com.

Top comments (0)