Building a Full-Stack Discord Clone with Next.js 13, Socket.IO, Prisma, and MySQL
Prerequisites
Before proceeding, ensure familiarity with:
- Core Next.js concepts (App Router, Server Componants)
- React fundamentals (hooks, component composition)
- Prisma ORM usage (schema definition, migrations)
- Tailwind CSS utility-first styling
- MySQL database modeling and connectivity
Project Initialization
Start a new Next.js application with TypeScript and Tailwind support:
npx create-next-app@latest discord-clone --typescript --tailwind --app --src-dir
Then integrate shadcn/ui components:
npx shadcn-ui@latest init
Follow the prompts to configure for Next.js App Router and Tailwind. After setup, launch the dev server:
npm run dev
Layout and Styling Adjustments
Update app/globals.css to enforce full viewport height across all root elements:
html,
body,
:root {
height: 100%;
}
Adopt a logical directory structure: group authentication-related routes under (auth) (a route group that doesn’t affect URL paths), and main application routes under (main). This improves maintainability without altering routing behavior.
Authentication with Clerk
Integrate Clerk for secure user management:
npm install @clerk/nextjs
Add required environment variables to .env.local:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_example_key
CLERK_SECRET_KEY=sk_test_example_secret
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
Create middleware.ts at the project root:
import { clerkMiddleware } from '@clerk/nextjs/server';
export default clerkMiddleware();
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
Wrap the root layout (app/layout.tsx) with ClerkProvider:
import { ClerkProvider } from '@clerk/nextjs';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
);
}
Auth Route Setup
Inside (auth), create dynamic route segments:
[[...sign-in]]/page.tsx:
'use client';
import { SignIn } from '@clerk/nextjs';
export default function SignInPage() {
return <SignIn />;
}
[[...sign-up]]/page.tsx:
'use client';
import { SignUp } from '@clerk/nextjs';
export default function SignUpPage() {
return <SignUp />;
}
Theme Management
Install and configure theme persistence:
npm install next-themes
Create components/providers/theme-provider.tsx:
'use client';
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ThemeProviderProps } from 'next-themes/dist/types';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
Update app/layout.tsx to wrap children with the provider:
import { ThemeProvider } from '@/components/providers/theme-provider';
// … inside body
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem={false}
storageKey="discord-theme"
>
{children}
</ThemeProvider>
Add a theme toggle component at components/mode-toggle.tsx:
'use client';
import * as React from 'react';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
If DropdownMenu is missing, scaffold it using:
npx shadcn-ui@latest add dropdown-menu
Database Layer with Prisma
Install Prisma CLI and initialize the ORM:
npm install -D prisma
npx prisma init
This generates prisma/schema.prisma and a .env file with DATABASE_URL. Configure the latter to point to your local or remote MySQL instance.