Tuesday, July 15, 2025
My Go-To API Strategy in Next.js: ky + TanStack Query
Posted by

Optimizing API Calls in Next.js with ky and TanStack Query
In this guide, I’ll walk you through how I handle API calls efficiently in production-grade apps using ky
and TanStack Query
, with clean patterns and fully typed responses. This is the exact structure I use in real-world apps.
Step 1: Create a Custom ky Instance
Step 1: Create the `kyInstance.ts` File
Inside your lib
folder, create a new file called kyInstance.ts
. This
will export a pre-configured ky
instance.
Step 2: Add Custom JSON Parsing
This instance will handle automatic parsing of any *At
fields into JS
Dates.
import ky from "ky";
const kyInstance = ky.create({
parseJson: (text) =>
JSON.parse(text, (key, value) => {
if (key.endsWith("At")) return new Date(value);
return value;
}),
});
export default kyInstance;
Step 2: Organize API Logic by Feature
Create a folder structure like this inside src/api
:
/src
/api
blogs-api.ts
Here’s an example using the blogs
feature:
import kyInstance from "@/lib/kyInstance";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
export interface Blog {
id: string;
title: string;
description: string;
cover: string;
date: string;
}
export interface BlogsResponse {
success: true;
data: Blog[];
}
export const fetchBlogs = async (): Promise<BlogsResponse> => {
return await kyInstance.get("/api/blogs").json();
};
export const useBlogs = () => {
return useQuery({
queryKey: ["blogs"],
queryFn: fetchBlogs,
});
};
Step 3: Create the Blog Index Page
Now let’s show the fetched data on the UI using a page in /app/blog/page.tsx
.
export default async function BlogIndexPage() {
const blogs = (await getAllBlogs()).sort(
(a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime()
);
return (
<div className="mx-auto flex min-h-[88vh] w-full flex-col gap-1 pt-2 sm:min-h-[91vh]">
<div className="mb-7 flex flex-col gap-2">
<h1 className="text-3xl font-extrabold">My latest blogs</h1>
<p className="text-muted-foreground">
All the latest blogs and news, straight from me.
</p>
</div>
<div className="mb-5 grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-8 md:grid-cols-3">
{blogs.map((blog) => (
<BlogCard {...blog} slug={blog.slug} key={blog.slug} />
))}
</div>
</div>
);
}
Step 4: Dynamic Blog Page with Full Content
Each blog page can be rendered dynamically using the [slug]
route. This allows SEO-friendly static pages.
export default async function BlogPage({ params: { slug } }: PageProps) {
const res = await getBlogForSlug(slug);
if (!res) notFound();
return (
<>
<div className="w-full">
<div className="mb-2 flex w-full flex-col gap-3 pb-7">
<h1 className="text-3xl font-extrabold sm:text-4xl">
{res.frontmatter.title}
</h1>
<div className="mt-6 flex flex-col gap-3">
<p className="text-sm text-muted-foreground">Posted by</p>
<Authors authors={res.frontmatter.authors} />
</div>
</div>
<div className="!w-full">
<div className="mb-7 w-full">
<Image
src={res.frontmatter.cover}
alt="cover"
width={700}
height={400}
className="h-[400px] w-full rounded-md border object-cover"
/>
</div>
<Typography>{res.content}</Typography>
</div>
</div>
</>
);
}
Summary
Using this pattern:
- API logic is colocated and type-safe
- Networking is abstracted using
ky
- Queries are reactive and cached with TanStack Query
- Frontend displays are consistent and optimized
This stack is scalable and production-ready, perfect for real-world full-stack applications.
Have questions or want to explore more real-world patterns? Ping me on GitHub!