CtrlK
BlogDocsLog inGet started
Tessl Logo

nextjs-project-starter

Scaffold a Next.js 15 App Router project with TypeScript, server/client component conventions, route groups, layouts, loading/error boundaries, Route Handlers, middleware, and SSR/SSG/ISR strategies.

85

Quality

80%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./frontend/nextjs-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Next.js Project Starter

Scaffold a Next.js 15 App Router project with TypeScript, server/client component conventions, route groups, layouts, loading/error boundaries, Route Handlers, middleware, and SSR/SSG/ISR strategies.

Prerequisites

  • Node.js >= 20.x
  • npm >= 10.x (or pnpm >= 9.x)
  • Git

Scaffold Command

npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd my-app

The CLI will prompt for options. Select:

  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • src/ directory: Yes
  • App Router: Yes
  • Turbopack for dev: Yes
  • Import alias: @/*

Project Structure

src/
├── app/
│   ├── (marketing)/           # Route group — no URL segment
│   │   ├── page.tsx           # Landing page (/)
│   │   ├── about/
│   │   │   └── page.tsx       # /about
│   │   └── layout.tsx         # Layout for marketing pages
│   ├── (app)/                 # Route group for authenticated app
│   │   ├── dashboard/
│   │   │   ├── page.tsx       # /dashboard
│   │   │   ├── loading.tsx    # Loading UI for dashboard
│   │   │   └── error.tsx      # Error boundary for dashboard
│   │   ├── settings/
│   │   │   └── page.tsx       # /settings
│   │   └── layout.tsx         # Shared layout with sidebar/nav
│   ├── api/
│   │   ├── health/
│   │   │   └── route.ts       # GET /api/health
│   │   └── users/
│   │       └── route.ts       # GET/POST /api/users
│   ├── layout.tsx             # Root layout (html, body, providers)
│   ├── not-found.tsx          # Custom 404 page
│   └── global-error.tsx       # Root error boundary
├── components/
│   ├── ui/                    # Shared UI primitives (Button, Card, etc.)
│   └── layout/                # Layout components (Header, Sidebar, Footer)
├── lib/
│   ├── db.ts                  # Database client
│   ├── auth.ts                # Auth helpers
│   └── utils.ts               # Shared utility functions
├── services/                  # Server-side data fetching functions
│   └── users.ts
├── types/                     # Shared TypeScript types
│   └── index.ts
├── middleware.ts               # Next.js middleware (root of src/)
└── styles/
    └── globals.css            # Tailwind + global styles
next.config.ts                 # Next.js configuration
.env.example                   # Required env vars template

Key Conventions

  • Server Components by default: every component in app/ is a Server Component unless it has "use client" at the top. Keep "use client" at the leaf level.
  • Route groups: use (groupName) folders for organizational grouping without affecting the URL structure.
  • Colocation: keep feature-specific components, types, and utilities close to the route that uses them. Shared things go in src/components/, src/lib/, src/types/.
  • Data fetching: fetch data in Server Components directly (no useEffect). Use fetch() with Next.js caching options or call DB/service functions directly.
  • loading.tsx: place next to page.tsx for automatic Suspense-based loading states.
  • error.tsx: place next to page.tsx for automatic error boundaries (must be "use client").
  • Route Handlers: use route.ts files in app/api/ for API endpoints. Export named functions (GET, POST, PUT, DELETE).
  • Metadata: export metadata or generateMetadata() from page/layout files for SEO.

Essential Patterns

Next.js Config (next.config.ts)

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // Enable React strict mode
  reactStrictMode: true,

  // Image optimization domains
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "example.com",
      },
    ],
  },

  // Redirect and rewrite examples
  async redirects() {
    return [
      {
        source: "/old-page",
        destination: "/new-page",
        permanent: true,
      },
    ];
  },

  // Environment variables exposed to the browser
  env: {
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  },
};

export default nextConfig;

Root Layout (Server Component)

// src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "@/styles/globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: {
    default: "My App",
    template: "%s | My App",
  },
  description: "A Next.js 15 application",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
}

Server Component (Data Fetching)

// src/app/(app)/dashboard/page.tsx
import type { Metadata } from "next";
import { getUsers } from "@/services/users";
import { UserList } from "./UserList";

export const metadata: Metadata = {
  title: "Dashboard",
};

export default async function DashboardPage() {
  const users = await getUsers();

  return (
    <main className="p-6">
      <h1 className="mb-4 text-2xl font-bold">Dashboard</h1>
      <UserList users={users} />
    </main>
  );
}

Client Component

// src/app/(app)/dashboard/UserList.tsx
"use client";

import { useState } from "react";

interface User {
  id: string;
  name: string;
  email: string;
}

export function UserList({ users }: { users: User[] }) {
  const [search, setSearch] = useState("");

  const filtered = users.filter((u) =>
    u.name.toLowerCase().includes(search.toLowerCase())
  );

  return (
    <div>
      <input
        type="text"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Search users..."
        className="mb-4 rounded border px-3 py-2"
      />
      <ul className="space-y-2">
        {filtered.map((user) => (
          <li key={user.id} className="rounded border p-3">
            <p className="font-medium">{user.name}</p>
            <p className="text-sm text-gray-500">{user.email}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

Loading State

// src/app/(app)/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="p-6">
      <div className="mb-4 h-8 w-48 animate-pulse rounded bg-gray-200" />
      <div className="space-y-2">
        {Array.from({ length: 5 }).map((_, i) => (
          <div key={i} className="h-16 animate-pulse rounded bg-gray-200" />
        ))}
      </div>
    </div>
  );
}

Error Boundary

// src/app/(app)/dashboard/error.tsx
"use client";

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="flex flex-col items-center justify-center gap-4 p-6">
      <h2 className="text-xl font-semibold text-red-600">Something went wrong</h2>
      <p className="text-gray-600">{error.message}</p>
      <button
        onClick={reset}
        className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
      >
        Try again
      </button>
    </div>
  );
}

Route Handler (API)

// src/app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getUsers, createUser } from "@/services/users";

export async function GET() {
  const users = await getUsers();
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  if (!body.name || !body.email) {
    return NextResponse.json(
      { error: "Name and email are required" },
      { status: 400 }
    );
  }

  const user = await createUser(body);
  return NextResponse.json(user, { status: 201 });
}

Middleware

// src/middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const token = request.cookies.get("session-token")?.value;

  // Protect /dashboard and /settings routes
  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  if (!token && request.nextUrl.pathname.startsWith("/settings")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};

Server Action

// src/app/(app)/settings/actions.ts
"use server";

import { revalidatePath } from "next/cache";

export async function updateProfile(formData: FormData) {
  const name = formData.get("name") as string;
  const email = formData.get("email") as string;

  // Validate and save to DB
  // await db.user.update({ ... });

  revalidatePath("/settings");
}

SSR / SSG / ISR Data Fetching Patterns

// SSR — fresh data on every request (default in App Router with dynamic data)
export default async function SSRPage() {
  const data = await fetch("https://api.example.com/data", {
    cache: "no-store",
  });
  return <div>{/* render data */}</div>;
}

// SSG — generated at build time
export default async function SSGPage() {
  const data = await fetch("https://api.example.com/data", {
    cache: "force-cache",
  });
  return <div>{/* render data */}</div>;
}

// ISR — revalidate every 60 seconds
export default async function ISRPage() {
  const data = await fetch("https://api.example.com/data", {
    next: { revalidate: 60 },
  });
  return <div>{/* render data */}</div>;
}

// Route-level revalidation config
export const revalidate = 60; // ISR: revalidate this page every 60s
export const dynamic = "force-dynamic"; // SSR: always render on request
export const dynamic = "force-static"; // SSG: always static

Static Params Generation (SSG for Dynamic Routes)

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetchPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await fetchPost(slug);
  return <article><h1>{post.title}</h1><div>{post.content}</div></article>;
}

Dynamic Metadata

// src/app/(app)/users/[id]/page.tsx
import type { Metadata } from "next";

interface Props {
  params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const user = await getUser(id);
  return {
    title: user.name,
    description: `Profile page for ${user.name}`,
  };
}

export default async function UserPage({ params }: Props) {
  const { id } = await params;
  const user = await getUser(id);
  return <div>{user.name}</div>;
}

First Steps After Scaffold

  1. Copy .env.example to .env.local and fill in values
  2. Install dependencies: npm install
  3. Start dev server: npm run dev
  4. Verify the app loads at http://localhost:3000
  5. Run npx tsc --noEmit to confirm TypeScript is clean

Common Commands

# Development
npm run dev                    # Start dev server with Turbopack (http://localhost:3000)

# Build & Production
npm run build                  # Create production build
npm run start                  # Start production server

# Lint
npm run lint                   # Run Next.js ESLint rules

# Type check
npx tsc --noEmit

Integration Notes

  • Database: pair with Prisma or Drizzle ORM skill. Import the DB client in src/lib/db.ts and call it directly in Server Components or Route Handlers.
  • Auth: pair with NextAuth.js (Auth.js v5) or Clerk skill. Middleware handles route protection; auth state is read in Server Components.
  • Testing: use Vitest + React Testing Library for component tests, Playwright for E2E. Next.js has no built-in test runner.
  • Deployment: Vercel (zero-config), or use output: "standalone" in next.config.ts for Docker/self-hosted.
  • State management: Server Components reduce the need for client state. For client-side state, use React context or Zustand. For server state, rely on Next.js caching + revalidatePath/revalidateTag.
  • Forms: Server Actions replace traditional API calls for mutations. Pair with Zod for validation.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.