Build a Fortnite Stats Tracker with Next.js and the Fortnite API
Full tutorial: build a Fortnite player stats tracker web app using Next.js and the Fortnite API. Covers account resolution, stats fetching, and server-side API key safety.
tutorialnextjsplayer-statsweb-app
What We're Building
A web app where users type any Fortnite display name and instantly see their stats. The key pattern: the Fortnite API uses Epic account IDs, so we always resolve a display name to an account ID first, then fetch stats.
Project Setup
npx create-next-app@latest fortnite-tracker --typescript --tailwind
cd fortnite-trackerAdd your API key to .env.local:
FORTNITE_API_KEY=your_key_hereAPI Route — Keep the Key Server-Side
Create app/api/stats/route.ts:
import { NextRequest, NextResponse } from "next/server";
const BASE_URL = "https://prod.api-fortnite.com";
const HEADERS = { "x-api-key": process.env.FORTNITE_API_KEY! };
export async function GET(req: NextRequest) {
const displayName = req.nextUrl.searchParams.get("displayName");
if (!displayName) {
return NextResponse.json({ error: "displayName required" }, { status: 400 });
}
// Step 1: resolve display name → Epic account ID
const accountRes = await fetch(
`${BASE_URL}/api/v1/account/displayName/${encodeURIComponent(displayName)}`,
{ headers: HEADERS }
);
if (!accountRes.ok) {
return NextResponse.json({ error: "Player not found" }, { status: 404 });
}
const account = await accountRes.json();
// Step 2: fetch stats with account ID
const statsRes = await fetch(
`${BASE_URL}/api/v2/stats/${account.id}`,
{ headers: HEADERS, next: { revalidate: 300 } } // cache 5 min
);
const stats = await statsRes.json();
return NextResponse.json({ account, stats });
}Search Component
"use client";
import { useState } from "react";
export default function StatsSearch() {
const [displayName, setDisplayName] = useState("");
const [result, setResult] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError("");
setResult(null);
const res = await fetch(
`/api/stats?displayName=${encodeURIComponent(displayName)}`
);
const json = await res.json();
if (!res.ok) {
setError(json.error ?? "Something went wrong");
} else {
setResult(json);
}
setLoading(false);
}
return (
<div className="max-w-xl mx-auto p-6">
<form onSubmit={handleSearch} className="flex gap-2 mb-6">
<input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Epic display name..."
className="flex-1 border rounded px-3 py-2"
/>
<button type="submit" disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded">
{loading ? "..." : "Search"}
</button>
</form>
{error && <p className="text-red-500">{error}</p>}
{result && (
<pre className="bg-gray-100 rounded p-4 text-sm overflow-auto">
{JSON.stringify(result, null, 2)}
</pre>
)}
</div>
);
}Ranked Stats
For level, XP, and ranked/arena progress, use the profile endpoints (requires Starter plan or above):
// Player level & XP
const progressRes = await fetch(
`${BASE_URL}/api/v1/profile/progress?displayName=${encodeURIComponent(displayName)}`,
{ headers: HEADERS }
);
// Ranked/arena progress
const rankedRes = await fetch(
`${BASE_URL}/api/v1/profile/ranked?displayName=${encodeURIComponent(displayName)}`,
{ headers: HEADERS }
);Deployment
npx vercel --prodSet FORTNITE_API_KEY in your Vercel project environment variables. Your tracker is live.