data-layer-migration
npx skills add https://github.com/blazity/next-migration-skills --skill data-layer-migration
Agent 安装分布
Skill 文档
Data Layer Migration
Migrate data-fetching patterns (getStaticProps, getServerSideProps, getStaticPaths) and API routes to App Router equivalents.
Toolkit Setup
This skill requires the nextjs-migration-toolkit skill to be installed. All migration skills depend on it for AST analysis.
TOOLKIT_DIR="$(cd "$(dirname "$SKILL_PATH")/../nextjs-migration-toolkit" && pwd)"
if [ ! -f "$TOOLKIT_DIR/package.json" ]; then
echo "ERROR: nextjs-migration-toolkit is not installed." >&2
echo "Run: npx skills add blazity/next-migration-skills -s nextjs-migration-toolkit" >&2
echo "Then retry this skill." >&2
exit 1
fi
bash "$TOOLKIT_DIR/scripts/setup.sh" >/dev/null
Version-Specific Patterns
Before applying any migration patterns, check the target Next.js version. Read .migration/target-version.txt if it exists, or ask the user.
Then read the corresponding version patterns file:
SKILL_DIR="$(cd "$(dirname "$SKILL_PATH")" && pwd)"
cat "$SKILL_DIR/../version-patterns/nextjs-<version>.md"
Critical version differences that affect data-layer migration:
- Next.js 14:
cookies()andheaders()are SYNCHRONOUS â do NOT useawait - Next.js 15+:
cookies()andheaders()are ASYNC â MUST useawait - Next.js 14: fetch is cached by default â use
{ cache: 'no-store' }for SSR replacement - Next.js 15+: fetch is NOT cached by default â SSR replacement needs no cache option
The examples below show Next.js 15+ patterns with await. If targeting Next.js 14, remove the await from cookies() and headers() calls.
Steps
1. Analyze Data Fetching Patterns
npx tsx "$TOOLKIT_DIR/src/bin/ast-tool.ts" transform data-fetching <sourceFile>
npx tsx "$TOOLKIT_DIR/src/bin/ast-tool.ts" analyze routes <pagesDir>
2. Migrate getStaticProps
Before:
import { GetStaticProps } from 'next';
export const getStaticProps: GetStaticProps = async () => {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return { props: { posts }, revalidate: 60 };
};
export default function BlogPage({ posts }) {
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
After (app/blog/page.tsx):
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
}).then(r => r.json());
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
Rules:
- Component is
asyncâ data fetching happens in the body - No revalidate:
{ cache: 'force-cache' }(the default in App Router) - With revalidate:
{ next: { revalidate: <seconds> } } - Remove the getStaticProps export entirely
- No props indirection â fetch result goes straight to JSX
3. Migrate getServerSideProps
Before:
import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async (context) => {
const { slug } = context.params;
const token = context.req.cookies.token;
const res = await fetch(`https://api.example.com/posts/${slug}`, {
headers: { Authorization: `Bearer ${token}` },
});
const post = await res.json();
return { props: { post } };
};
export default function PostPage({ post }) {
return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}
After (app/blog/[slug]/page.tsx):
import { cookies } from 'next/headers';
export default async function PostPage({ params }: { params: { slug: string } }) {
const cookieStore = await cookies();
const token = cookieStore.get('token')?.value;
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
cache: 'no-store',
headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json());
return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}
Rules:
context.paramsâparamsprop on the page componentcontext.queryâsearchParamspropcontext.req.cookiesâcookies()fromnext/headerscontext.req.headersâheaders()fromnext/headers- Always use
{ cache: 'no-store' }to match getServerSideProps behavior
4. Migrate getStaticPaths
Before:
import { GetStaticPaths, GetStaticProps } from 'next';
export const getStaticPaths: GetStaticPaths = async () => {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
paths: posts.map((p) => ({ params: { slug: p.slug } })),
fallback: 'blocking',
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());
return { props: { post } };
};
After (app/blog/[slug]/page.tsx):
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((p) => ({ slug: p.slug }));
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());
return <article><h1>{post.title}</h1></article>;
}
Rules:
- Return flat param objects (not wrapped in
{ params: {} }) fallback: 'blocking'/fallback: trueâ handled automatically by App Router (addloading.tsxfor a loading UI)fallback: falseâ unconverted paths will 404 by default
5. Migrate API Routes
Before (pages/api/posts.ts):
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const { title, content } = req.body;
const post = await db.posts.create({ title, content });
return res.status(201).json(post);
}
const posts = await db.posts.findMany();
return res.status(200).json(posts);
}
After (app/api/posts/route.ts):
import { NextRequest, NextResponse } from 'next/server';
export async function GET() {
const posts = await db.posts.findMany();
return NextResponse.json(posts);
}
export async function POST(request: NextRequest) {
const { title, content } = await request.json();
const post = await db.posts.create({ title, content });
return NextResponse.json(post, { status: 201 });
}
Rules:
- One file per route, named
route.ts(not the handler name) - Separate named exports per HTTP method (GET, POST, PUT, DELETE, PATCH)
req.bodyâawait request.json()res.status(N).json(data)âNextResponse.json(data, { status: N })res.redirect(url)âNextResponse.redirect(new URL(url, request.url))
6. Validate Data Layer
npx tsx "$TOOLKIT_DIR/src/bin/ast-tool.ts" validate <appDir>
Verify no old data-fetching patterns remain in app/ directory.
Common Pitfalls
Forgetting { cache: 'no-store' } on getServerSideProps replacements
Symptom: Page shows stale data or behaves like a static page.
Cause: App Router defaults to { cache: 'force-cache' }. Without { cache: 'no-store' }, the fetch result is cached at build time â the opposite of getServerSideProps behavior.
Fix: Always add { cache: 'no-store' } when replacing getServerSideProps.
Using req / res objects in route handlers
Error: req is not defined or type errors on NextApiRequest.
Cause: App Router route handlers use NextRequest (Web API based), not NextApiRequest (Node.js based).
Fix: Replace req.body â await request.json(), req.query â request.nextUrl.searchParams, res.json() â NextResponse.json().
Async cookies() and headers()
Error: cookies() or headers() returning a Promise instead of the value directly.
Cause: In Next.js 15+, cookies() and headers() are async. In Next.js 13-14, they were synchronous.
Fix: Use const cookieStore = await cookies() and then cookieStore.get('name').
Mixing getStaticProps and getServerSideProps patterns
Wrong: Using { cache: 'force-cache' } with dynamic data that changes on every request.
Right: Match the caching strategy to the original behavior:
- getStaticProps (no revalidate) â
{ cache: 'force-cache' }or omit (default) - getStaticProps (with revalidate) â
{ next: { revalidate: N } } - getServerSideProps â
{ cache: 'no-store' }
Route handler returning wrong type
Error: Route handlers must return a Response object.
Fix: Always return NextResponse.json(data), NextResponse.redirect(), or new Response(body). Don’t return plain objects.
Converting server data fetching into client-side useEffect + fetch
Wrong: The page had getServerSideProps that fetched data. You make the page 'use client' and replace the data fetching with useEffect:
// WRONG â data only appears after JS executes on the client
'use client';
import { useState, useEffect } from 'react';
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
if (!data) return <p>Loading...</p>;
return <div>{data.value}</div>;
}
Right: Keep data fetching in an async server component. Only extract interactive UI into a client component.
// app/page.tsx (server component)
export default async function Page() {
const data = await fetch('https://api.example.com/data', { cache: 'no-store' }).then(r => r.json());
return <div>{data.value}</div>;
}
Rule: NEVER replace getServerSideProps or getStaticProps with useEffect + fetch in a client component. The data fetching MUST remain server-side in an async server component. The only reason to add 'use client' is for interactive UI (hooks, event handlers), and that component receives already-fetched data as props.