Overview
This page traces two complete data flows through the application:
- Fetching Pokemon List - Used on the home page gallery
- Fetching Pokemon Detail - Used on individual Pokemon pages
Each example shows the exact code path from component to API and back.
Example 1: Fetching Pokemon List
This flow demonstrates how the home page fetches and displays a list of Pokemon.
Step 1: Component Calls Service
File: src/app/page.tsx:19-22
export default async function Home() {
// Component calls service layer
const { data: pokemonList, error } = await getPokemonListGQL()
if (error) throw new Error(JSON.stringify(error))
return (
<main className="flex flex-col gap-8 min-h-screen py-12 px-[4%] md:px-[12%] lg:px-[20%]">
<div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-4xl lg:text-6xl font-bold uppercase font-rajdhani text-zinc-300">
Explore
</h1>
<p className="text-zinc-500 text-md">
Discover information and statistics about your favorite Pokémon.
</p>
</div>
<PokeGallery content={pokemonList} />
</main>
)
}
The component:
- Calls
getPokemonListGQL() from the service layer
- Destructures the
ServiceResponse into data and error
- Checks for errors before rendering
- Passes clean
PokemonSummary[] data to child components
File: src/services/pokemon.service.ts:56-73
export const getPokemonListGQL = async (): Promise<
ServiceResponse<PokemonSummary[]>
> => {
try {
// Call fetcher to get raw API data
const pokemonList = await fetchPokemonListGQL()
if (!pokemonList || pokemonList.data.pokemon.length === 0) {
throw new ApiError('Pokémon list is null')
}
// Transform each item using adapter
const data = pokemonList.data.pokemon.map((p) => adaptPokemonSummary(p))
// Return normalized response
return { data, error: null }
} catch (error) {
const fault = handleServiceError(error, '[getPokemonList]')
return {
data: null,
error: fault,
}
}
}
The service:
- Calls
fetchPokemonListGQL() to get raw GraphQL data
- Validates the response
- Maps each raw item through
adaptPokemonSummary() adapter
- Wraps result in
ServiceResponse format
- Catches and normalizes all errors
Step 3: Fetcher Makes GraphQL Request
File: src/lib/api/pokemon.api.ts:105-140
export const fetchPokemonListGQL = async (
limit: number = Number(LIMIT)
): Promise<GQLPokemonSummaryList> => {
// Define GraphQL query
const query = `
query getPokemonList($limit: Int, $offset: Int) {
pokemon(limit: $limit, offset: $offset) {
id
name
pokemontypes {
type {
name
}
}
}
}
`
// Make POST request to GraphQL endpoint
const response = await fetch(GQL_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables: { limit, offset: 0 },
}),
next: { revalidate: 604800 }, // Cache for 7 days
})
if (!response.ok) {
throw new ApiError(
`[API.ERROR] The Pokémon list could not be obtained.`,
response.status,
'[fetchPokemonList]'
)
}
return await response.json()
}
The fetcher:
- Constructs GraphQL query with variables
- Makes HTTP POST request to GraphQL endpoint
- Implements Next.js ISR caching (7 days)
- Handles HTTP errors with
ApiError
- Returns raw
GQLPokemonSummaryList type
Step 4: API Returns Raw Data
The GraphQL API returns data in this structure:
{
"data": {
"pokemon": [
{
"id": 1,
"name": "bulbasaur",
"pokemontypes": [
{ "type": { "name": "grass" } },
{ "type": { "name": "poison" } }
]
},
{
"id": 2,
"name": "ivysaur",
"pokemontypes": [
{ "type": { "name": "grass" } },
{ "type": { "name": "poison" } }
]
}
]
}
}
File: src/adapters/pokemon-summary.adapter.ts:3-18
export const adaptPokemonSummary = ({
id,
name,
pokemontypes,
}: GQLPokemonSummaryList['data']['pokemon'][number]): PokemonSummary => {
// Extract type names from nested structure
const types = pokemontypes.map(
(t) => t.type.name as PokemonSummary['types'][number]
)
return {
id,
name,
types,
// Construct image URL from Pokemon ID
image: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/${id || 132}.png`,
}
}
The adapter:
- Flattens nested
pokemontypes array into simple string array
- Generates image URL based on Pokemon ID
- Provides fallback ID (132 = Ditto) if missing
- Returns clean
PokemonSummary object
Step 6: Clean Data Flows Back to Component
After transformation, the component receives:
const pokemonList: PokemonSummary[] = [
{
id: 1,
name: "bulbasaur",
types: ["grass", "poison"],
image: "https://raw.githubusercontent.com/.../1.png"
},
{
id: 2,
name: "ivysaur",
types: ["grass", "poison"],
image: "https://raw.githubusercontent.com/.../2.png"
}
// ... more pokemon
]
Notice how the component never sees the raw API structure with nested pokemontypes. It only works with the clean PokemonSummary type.
Example 2: Fetching Pokemon Detail
This flow demonstrates fetching detailed information for a single Pokemon (used on /pokemon/[slug] pages).
Step 1: Component Calls Service
File: src/app/pokemon/[slug]/page.tsx:38-44
export default async function PokemonDetailPage({ params }: Props) {
const { slug } = await params
// Call service with Pokemon slug
const { data: pokemonData, error } = await getPokemonDetail(slug)
// Validate response
if (error && error.code != 404) throw new Error(JSON.stringify(error))
if (!pokemonData) notFound()
const type = getMostColorfulType(pokemonData.types.map((t) => t.name))
const theme = POKE_THEMES[type]
return (
<main className="w-full min-h-screen flex flex-col items-center gap-12">
<PokemonDetailView data={pokemonData} />
{pokemonData.evolution && (
<Suspense fallback={<EvolutionChainSkeleton />}>
<EvolutionChain theme={theme} id={pokemonData.evolution.id} />
</Suspense>
)}
</main>
)
}
The component:
- Calls
getPokemonDetail(slug) with the Pokemon name/ID
- Handles 404 errors specially (shows not found page)
- Throws on other errors (caught by error boundary)
- Uses strongly-typed
PokemonDetail data
Step 2: Service Validates & Orchestrates
File: src/services/pokemon.service.ts:20-36
export const getPokemonDetail = async (
slug: string,
extended: boolean = true
): Promise<ServiceResponse<PokemonDetail>> => {
try {
// Validate input
if (!slug) throw new Error('The Pokémon slug or ID is required.')
// Fetch raw data from API
const pokemonData = await fetchPokemonByID(slug, extended)
if (!pokemonData) throw new ApiError('Pokémon data is null')
// Transform using adapter and return
return { data: adaptPokemon(pokemonData), error: null }
} catch (error) {
const fault = handleServiceError(error, '[getPokemonDetail]')
return {
data: null,
error: fault,
}
}
}
The service:
- Validates the
slug parameter
- Calls
fetchPokemonByID() with extended=true for full data
- Passes raw response through
adaptPokemon() adapter
- Returns normalized
ServiceResponse
Step 3: Fetcher Makes Multiple API Calls
File: src/lib/api/pokemon.api.ts:15-88
export const fetchPokemonByID = async (
slug: string,
extended = false
): Promise<ApiPokemonResponse> => {
// 1. Fetch base Pokemon data
const baseResponse = await fetch(`${BASE_URL}/pokemon/${slug}`, {
next: { revalidate: 604800 },
})
if (!baseResponse.ok) {
throw new ApiError(
`[API.ERROR] The Pokémon "${slug}" could not be obtained.`,
baseResponse.status,
'[fetchPokemonByID.base]'
)
}
const baseData: ApiPokemonResponse = await baseResponse.json()
if (!extended) return baseData
// 2. Fetch species data (for descriptions, genus, etc.)
const speciesResponse = await fetch(baseData.species.url)
if (!speciesResponse.ok) {
throw new ApiError(
`[API.ERROR] The Pokémon "${slug}" could not be obtained.`,
speciesResponse.status,
'[fetchPokemonByID.species]'
)
}
const speciesData: ApiSpeciesResponse = await speciesResponse.json()
// 3. Fetch data for each variety in parallel
const varietyData = await Promise.all(
speciesData.varieties.map(async (variety) => {
if (variety.pokemon.name === baseData.name) {
// Use base data for default variety
return {
...variety,
types: baseData.types,
stats: baseData.stats,
abilities: baseData.abilities,
weight: baseData.weight,
height: baseData.height,
}
}
// Fetch full data for alternate varieties
try {
const res = await fetch(variety.pokemon.url, {
next: { revalidate: 604800 },
})
if (res.ok) {
const data: ApiPokemonResponse = await res.json()
return {
...variety,
types: data.types,
stats: data.stats,
abilities: data.abilities,
weight: data.weight,
height: data.height,
}
}
} catch (e) {
console.error(
`[API.ERROR] Could not fetch variety ${variety.pokemon.name}`,
e
)
}
return variety
})
)
// 4. Combine all data and return
return {
...baseData,
genera: speciesData.genera,
flavor_text_entries: speciesData.flavor_text_entries,
evolution_chain: speciesData.evolution_chain,
varieties: varietyData,
}
}
The fetcher makes multiple API calls when extended=true:
- Base Pokemon data (stats, types, sprites)
- Species data (descriptions, genus, evolution chain URL)
- Variety data (for alternate forms like Alolan, Galarian, etc.)
All variety fetches happen in parallel using Promise.all().
Step 4: API Returns Complex Nested Data
The raw API response looks like:
{
"id": 25,
"name": "pikachu",
"height": 4,
"weight": 60,
"types": [
{
"type": {
"name": "electric",
"url": "https://pokeapi.co/api/v2/type/13/"
}
}
],
"stats": [
{ "base_stat": 35, "stat": { "name": "hp" } },
{ "base_stat": 55, "stat": { "name": "attack" } },
{ "base_stat": 40, "stat": { "name": "defense" } },
{ "base_stat": 50, "stat": { "name": "special-attack" } },
{ "base_stat": 50, "stat": { "name": "special-defense" } },
{ "base_stat": 90, "stat": { "name": "speed" } }
],
"sprites": {
"other": {
"official-artwork": {
"front_default": "https://...",
"front_shiny": "https://..."
},
"home": {
"front_default": "https://...",
"front_shiny": "https://..."
}
}
},
"genera": [
{ "genus": "Mouse Pokémon", "language": { "name": "en" } }
],
"flavor_text_entries": [
{
"flavor_text": "When several of these POKéMON gather, their electricity could build and cause lightning storms.",
"language": { "name": "en" }
}
],
"evolution_chain": {
"url": "https://pokeapi.co/api/v2/evolution-chain/10/"
}
}
File: src/adapters/pokemon-detail.adapter.ts:10-67
export const adaptPokemon = ({
id,
name,
height,
weight,
types,
sprites,
flavor_text_entries,
genera,
abilities: apiAbilities,
stats: apiStats,
evolution_chain,
varieties: apiVarieties,
}: ApiPokemonResponse): PokemonDetail => {
// Extract sprites
const artwork = sprites.other['official-artwork']
const home = sprites.other.home
const dummyImage = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/132.png'
// Transform nested data using helper functions
const mappedTypes = mapTypes(types)
const genus = distillGenus(genera) // Extracts English genus
const abilities = mapAbilities(apiAbilities)
const description = distillDescription(flavor_text_entries) // Extracts & cleans English text
const stats = mapStats(apiStats) // Maps to HP, ATK, DEF, etc.
// Extract evolution chain ID from URL
const evolution = evolution_chain && evolution_chain.url
? {
id: distillEvolutionChainId(evolution_chain.url),
url: evolution_chain.url,
chain: [],
}
: null
const varieties = mapVarieties(apiVarieties || [], genus, description)
return {
id,
name,
genus,
types: mappedTypes,
abilities,
description,
height: height / 10, // Decimeters → meters
weight: weight / 10, // Hectograms → kilograms
stats,
evolution,
varieties,
assets: {
official: {
default: artwork.front_default || dummyImage,
shiny: artwork.front_shiny || dummyImage,
},
home: {
default: home.front_default || dummyImage,
shiny: home.front_shiny || dummyImage,
},
},
}
}
The adapter:
- Extracts deeply nested sprite URLs
- Filters localized data to English only
- Converts units (decimeters to meters, hectograms to kg)
- Maps API stat names (
"special-attack") to app names ("SPA")
- Cleans flavor text (removes newlines, extra spaces)
- Extracts evolution chain ID from URL string
- Provides fallback images for missing sprites
Step 6: Clean, Type-Safe Data Returns to Component
The component receives:
const pokemonData: PokemonDetail = {
id: 25,
name: "pikachu",
genus: "Mouse",
height: 0.4, // meters
weight: 6.0, // kilograms
description: "When several of these POKéMON gather, their electricity could build and cause lightning storms.",
types: [
{ name: "electric", url: "https://pokeapi.co/api/v2/type/13/" }
],
stats: [
{ name: "HP", value: 35 },
{ name: "ATK", value: 55 },
{ name: "DEF", value: 40 },
{ name: "SPA", value: 50 },
{ name: "SPD", value: 50 },
{ name: "SPE", value: 90 }
],
abilities: [
{ name: "static", hidden: false },
{ name: "lightning-rod", hidden: true }
],
assets: {
official: {
default: "https://...",
shiny: "https://..."
},
home: {
default: "https://...",
shiny: "https://..."
}
},
evolution: {
id: 10,
url: "https://pokeapi.co/api/v2/evolution-chain/10/",
chain: []
},
varieties: []
}
The component receives clean, flat data with:
- Correct units (meters, kilograms)
- Clean, single-line descriptions
- Abbreviated stat names (HP, ATK, DEF)
- Direct sprite URLs
- Extracted evolution chain ID
Visual Flow Diagram
Key Takeaways
Components Stay Clean
Components never handle raw API structures, unit conversions, or data extraction.
Services Never Throw
All errors are caught and returned as ServiceResponse, making error handling predictable.
Adapters Isolate Changes
If PokeAPI changes its structure, only adapters need updates.
Type Safety Throughout
TypeScript enforces correct types at every layer boundary.
Related Pages
Architecture Overview
High-level explanation of the architecture pattern
Clean Architecture Details
In-depth look at each layer with complete code examples