Back to blog

Tuesday, July 15, 2025

My Go-To API Strategy in Next.js: ky + TanStack Query

cover

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

1

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.

2

Step 2: Add Custom JSON Parsing

This instance will handle automatic parsing of any *At fields into JS Dates.

/lib/kyInstance.ts
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:

/src/api/blogs-api.ts
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.

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.

/app/blog/[slug]/page.tsx
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!