A mini social media app inspired by GitHub + Pantip built with Next.js App Router, Better Auth, Prisma/MongoDB, and Tailwind CSS.
All commands run from the project root:
# Development
npm run dev # Start dev server at http://localhost:3000
# Production
npm run build # Production build
npm run start # Start production server
# Linting
npm run lint # Run Next.js ESLint
# Database
npx prisma db push # Sync Prisma schema to database
npx prisma studio # Open Prisma database GUI
npx prisma generate # Generate Prisma client (runs on postinstall)There is currently no test suite - do not attempt to run tests.
mypost-but-github/
├── actions/ # Server Actions ("use server")
├── app/ # Next.js App Router pages
├── components/ # React components
│ ├── ui/ # shadcn/ui base components
│ ├── nav/ # Navigation components
│ ├── posts/ # Post-related components
│ └── comments/ # Comment components
├── hooks/ # Custom React hooks
├── lib/ # Utilities (prismadb, utils)
├── providers/ # React context providers
├── prisma/ # Database schema
├── public/ # Static assets
├── store/ # Zustand stores
├── types/ # TypeScript types and Zod validators
├── site/ # Site metadata
└── data/ # Data utilities
- Use path aliases:
@/*(mapped to root) - Order imports: external → internal → relative
- Example:
import { useState } from "react"; import { z } from "zod"; import { db as prisma } from "@/lib/prismadb"; import { PostValidator } from "@/types"; import { createPost } from "@/actions/post-actions"; import { cn } from "@/lib/utils";
-
Use strict mode (
noUncheckedIndexedAccess: trueenabled) -
Define types in
types/index.tsusing Zod validators -
Use interfaces for object shapes, types for unions/primitives
-
Example pattern:
import { z } from "zod"; export const PostValidator = z.object({ title: z.string().min(1).max(256), body: z.string().min(1), tag: z.string().optional(), }); export type Post = z.infer<typeof PostValidator>;
- Files: kebab-case (
post-actions.ts,Nav.tsx) - Components: PascalCase (
PostCard.tsx,Nav.tsx) - Functions/Variables: camelCase
- Constants: SCREAMING_SNAKE_CASE
- Server Actions: camelCase with descriptive names (
createPost,toggleLike)
- Use functional components with TypeScript
- Props interface named
{ComponentName}Props - Use
cn()from@/lib/utilsfor className merging - Extract reusable logic into custom hooks in
hooks/ - Server components by default, add
"use client"only when needed
Example:
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "outline";
}
export function Button({ className, variant = "default", ...props }: ButtonProps) {
return (
<button
className={cn("px-4 py-2", variant === "outline" && "border", className)}
{...props}
/>
);
}- Always mark with
"use server"at top of file - Handle errors with try/catch
- Return typed results (union types for success/error)
- Use Zod for input validation
- Call
revalidatePath()after mutations
Example:
export type ToggleLikeResult = { hasLiked: boolean } | { error: string };
export const toggleLike = async (postId: string): Promise<ToggleLikeResult> => {
const { userId } = await auth();
try {
if (!userId) return { error: "Unauthorized" };
// ... mutation logic
return { hasLiked: true };
} catch (error) {
console.log(error);
return { error: "Internal server error" };
}
};- Use try/catch in server actions
- Return error objects rather than throwing for expected failures
- Throw for unexpected errors
- Log errors with
console.log(error)in server actions - Use Zod validation errors:
if (error instanceof z.ZodError) throw new Error(error.message)
- Server state: TanStack Query for data fetching/caching
- Client state: Zustand stores in
store/ - Form state: React Hook Form with Zod resolver
- Use
revalidatePath()after server mutations
- Use Tailwind CSS v4 (no separate config file, uses CSS variables)
- Use
cn()utility for conditional classes - Use shadcn/ui components from
components/ui/ - Avoid custom CSS; use Tailwind utilities
- Use Better Auth (
better-auth) - Server:
import { auth } from "@/lib/auth"thenauth.api.getSession({ headers: await headers() }) - Client:
import { authClient } from "@/lib/auth-client"thenauthClient.useSession() - Login modal:
import { LoginModal } from "@/components/auth/LoginModal" - Protect routes by checking
session?.user?.id
| Category | Library |
|---|---|
| Framework | Next.js 16 (App Router) |
| UI | React 19, Tailwind CSS 4, Radix UI |
| Auth | Better Auth |
| Database | Prisma 6, MongoDB |
| State | TanStack Query, Zustand |
| Forms | React Hook Form, Zod |
| Editor | BlockNote |
Create .env with:
DATABASE_URL="mongodb+srv://..."
BETTER_AUTH_SECRET="..."
BETTER_AUTH_URL="http://localhost:3000"
GOOGLE_CLIENT_ID="..."
GOOGLE_CLIENT_SECRET="..."
GITHUB_CLIENT_ID="..."
GITHUB_CLIENT_SECRET="..."- The project uses Next.js 16 with React 19 - watch for breaking changes
- MongoDB Atlas Search is optional - app falls back to Prisma
containsif unavailable - No test framework is configured - focus on manual testing
- Use
prisma generateafter modifying schema (runs onnpm install) - The app is deployed on Vercel (see live demo in README)