nextjs-fullstack

Opinionated Next.js fullstack patterns — App Router, Tailwind CSS v4, shadcn/ui, Better Auth, Drizzle ORM, Server Actions, and Vercel deployment. Use when scaffolding a new project or enforcing consistent architecture across client projects.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "nextjs-fullstack" with this command: npx skills add saccoai/agent-skills/saccoai-agent-skills-nextjs-fullstack

This skill codifies an opinionated fullstack Next.js stack for building production web applications. It defines architecture patterns, file conventions, and best practices to ensure consistency across projects.

Use when: starting a new project, onboarding a developer to the stack, or reviewing code for pattern compliance.

The Stack

LayerTechnologyVersionPurpose
FrameworkNext.js16+App Router, RSC, Server Actions
LanguageTypeScript5.xStrict mode enabled
StylingTailwind CSSv4Utility-first, CSS variables
Componentsshadcn/uilatestCopy-paste primitives, Radix-based
AuthBetter AuthlatestEmail/password, OAuth, sessions
DatabaseDrizzle ORMlatestType-safe SQL, migrations
DB ProviderPostgreSQL(via Neon/Supabase/local)Production database
AnimationsFramer MotionlatestLazyMotion + m for tree-shaking
FormsReact Hook Form + ZodlatestValidation, Server Actions
EmailNodemailerlatestTransactional emails
HostingVercelAuto-deploy, edge functions

Project Structure

src/
├── app/
│   ├── (auth)/              # Auth route group
│   │   ├── login/page.tsx
│   │   ├── register/page.tsx
│   │   └── layout.tsx       # Auth layout (centered, minimal)
│   ├── (dashboard)/         # Protected route group
│   │   ├── dashboard/page.tsx
│   │   └── layout.tsx       # Dashboard layout (sidebar)
│   ├── (marketing)/         # Public route group
│   │   ├── page.tsx         # Homepage
│   │   ├── about/page.tsx
│   │   └── layout.tsx       # Marketing layout (header + footer)
│   ├── api/
│   │   ├── auth/[...all]/route.ts  # Better Auth handler
│   │   └── webhooks/               # External webhooks
│   ├── layout.tsx           # Root layout (providers, fonts, metadata)
│   ├── globals.css          # Tailwind imports, CSS variables
│   ├── sitemap.ts           # Dynamic sitemap
│   └── robots.ts            # Robots config
├── components/
│   ├── ui/                  # shadcn/ui components (don't edit)
│   ├── layout/              # Header, footer, sidebar, nav
│   ├── forms/               # Form components with validation
│   └── sections/            # Page section components
├── data/                    # Static content data (TypeScript)
├── lib/
│   ├── auth.ts              # Better Auth client instance
│   ├── auth-server.ts       # Better Auth server instance
│   ├── db/
│   │   ├── index.ts         # Drizzle client
│   │   ├── schema.ts        # Database schema
│   │   └── migrations/      # SQL migrations
│   ├── utils.ts             # cn() and shared utilities
│   └── validations/         # Zod schemas (shared client + server)
├── hooks/                   # Custom React hooks
├── types/                   # TypeScript type definitions
└── actions/                 # Server Actions
    ├── auth.ts
    └── {resource}.ts

Architecture Patterns

Server Components by Default

Every component is a Server Component unless it needs interactivity:

// src/app/page.tsx — Server Component (default)
import { db } from "@/lib/db";

export default async function HomePage() {
  const posts = await db.query.posts.findMany();
  return <PostList posts={posts} />;
}

Add "use client" only for:

  • Event handlers (onClick, onChange, onSubmit)
  • useState, useEffect, useRef
  • Browser APIs (window, document)
  • Third-party client libraries

Server Actions for Mutations

Use Server Actions instead of API routes for data mutations:

// src/actions/contact.ts
"use server";

import { z } from "zod";
import { contactSchema } from "@/lib/validations/contact";

export async function submitContact(formData: FormData) {
  const parsed = contactSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
    message: formData.get("message"),
  });

  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors };
  }

  // Send email, save to DB, etc.
  return { success: true };
}

API Routes Only For

  • Webhooks from external services
  • Auth handlers (Better Auth)
  • Public APIs consumed by third parties
  • File uploads

Route Groups for Layout Separation

(marketing)/  → public pages with header/footer
(auth)/       → login/register with minimal centered layout
(dashboard)/  → protected pages with sidebar, requires auth

Metadata Pattern

Every page exports metadata. For "use client" pages, use a sibling layout:

// src/app/about/page.tsx (Server Component)
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About",
  description: "About our company",
};

export default function AboutPage() { ... }
// src/app/contact/layout.tsx (for client pages)
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Contact",
  description: "Get in touch",
};

export default function Layout({ children }: { children: React.ReactNode }) {
  return children;
}

Authentication Pattern (Better Auth)

// src/lib/auth-server.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/lib/db";

export const auth = betterAuth({
  database: drizzleAdapter(db),
  emailAndPassword: { enabled: true },
  // ... providers
});

// src/lib/auth.ts (client)
import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient();

Database Pattern (Drizzle ORM)

// src/lib/db/schema.ts
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";

export const posts = pgTable("posts", {
  id: uuid("id").primaryKey().defaultRandom(),
  title: text("title").notNull(),
  content: text("content"),
  createdAt: timestamp("created_at").defaultNow(),
});

// src/lib/db/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

Validation Pattern (Zod — Shared Client + Server)

// src/lib/validations/contact.ts
import { z } from "zod";

export const contactSchema = z.object({
  name: z.string().min(2, "Name is required"),
  email: z.string().email("Invalid email"),
  message: z.string().min(10, "Message too short"),
});

export type ContactInput = z.infer<typeof contactSchema>;

Used in both Server Actions (server-side validation) and React Hook Form (client-side validation).

Animation Pattern (Framer Motion — Tree-Shakeable)

// src/components/sections/animated-section.tsx
"use client";

import { LazyMotion, domAnimation, m } from "framer-motion";

export function AnimatedSection({ children }: { children: React.ReactNode }) {
  return (
    <LazyMotion features={domAnimation}>
      <m.div
        initial={{ opacity: 0, y: 20 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, margin: "-100px" }}
        transition={{ duration: 0.5 }}
      >
        {children}
      </m.div>
    </LazyMotion>
  );
}

Always use LazyMotion + m (not motion) for smaller bundles.

Tailwind CSS v4 Setup

/* src/app/globals.css */
@import "tailwindcss";

@theme {
  --color-primary: #8e375c;
  --color-primary-dark: #6d2847;
  --font-heading: "Playfair Display", serif;
  --font-body: "Inter", sans-serif;
}

Naming Conventions

ItemConventionExample
Fileskebab-casepage-hero.tsx
ComponentsPascalCasePageHero
HookscamelCase with use prefixuseAuth
Server ActionscamelCase verbsubmitContact
Zod schemascamelCase + Schema suffixcontactSchema
DB tablessnake_case (plural)blog_posts
DB columnssnake_casecreated_at
CSS variableskebab-case with -- prefix--color-primary
Env varsSCREAMING_SNAKE_CASEDATABASE_URL

Common Commands

# Development
npm run dev              # Start dev server
npm run build            # Production build
npm run lint             # ESLint

# Database
npx drizzle-kit generate   # Generate migration from schema changes
npx drizzle-kit migrate    # Apply migrations
npx drizzle-kit studio     # Open Drizzle Studio (DB browser)

# shadcn/ui
npx shadcn@latest add button   # Add a component

# Deployment
npx vercel                 # Deploy preview
npx vercel --prod          # Deploy production

Environment Variables Template

# Database
DATABASE_URL=postgresql://user:pass@host:5432/dbname

# Auth
BETTER_AUTH_SECRET=generate-a-random-string
BETTER_AUTH_URL=http://localhost:3000

# Email
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=user@example.com
SMTP_PASS=your-password
SMTP_FROM=noreply@example.com

# Optional
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX

Agent Team Integration

This skill is used by the designer teammate in the website-refactor workflow to ensure consistent architecture. It can also be loaded by any teammate that needs to understand the project's patterns.

Anti-Patterns (Don't Do This)

  • API routes for internal mutations — Use Server Actions instead
  • "use client" on pages — Keep pages as Server Components; extract client parts into child components
  • Importing motion directly — Always use LazyMotion + m for tree-shaking
  • Raw SQL — Use Drizzle ORM for type-safe queries
  • any types — TypeScript strict mode is enabled; type everything
  • Inline styles — Use Tailwind classes
  • .env in git — Only .env.example is committed

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

client-proposal

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

project-handoff

No summary provided by upstream source.

Repository SourceNeeds Review
Research

website-analysis

No summary provided by upstream source.

Repository SourceNeeds Review