Master the T3 Stack and Build a YouTube Clone

Are you a developer looking to take your full-stack web development skills to the next level? Learning the T3 stack and building a complex, real-world application like a YouTube clone is a great way to do just that.

The T3 stack is a modern, powerful toolkit for building full-stack TypeScript apps. It consists of:

  • Next.js: A React framework for server-side rendering and API routes
  • TypeScript: A typed superset of JavaScript for improved tooling and bug prevention
  • tRPC: End-to-end type-safe APIs without code generation
  • NextAuth: Authentication for Next.js
  • Prisma: Type-safe database access and migrations
  • Tailwind CSS: Utility-first CSS framework for rapid UI development

When used together, these tools enable developers to build high-quality, type-safe, full-featured web apps extremely efficiently. Next.js provides a great development experience out of the box. TypeScript adds type safety across the stack. tRPC allows defining APIs with full type safety and no code generation. NextAuth makes authentication simple. Prisma unlocks the power of SQL with an intuitive data model and type-safe queries. And Tailwind CSS makes it a breeze to style the UI consistently.

Let‘s walk through the process of using the T3 stack to build a YouTube clone from scratch. We‘ll build all the core features you‘d expect:

  • Home page with featured videos and categories
  • Search and filter capabilities
  • Dedicated page for each video with details and related videos
  • User registration and login with NextAuth
  • Profiles with user details and published videos
  • Liking, disliking, commenting on videos
  • Playlists and watch history
  • Admin dashboard for publishing and managing videos

We‘ll use best practices throughout to create a maintainable, scalable foundation you can build on and learn from. Let‘s get started!

Setting Up a New T3 Project

The first step is to set up a new T3 project. We can do this easily using the official create-t3-app CLI tool. In your terminal, run:

npx create-t3-app@latest

This will walk you through the process of creating a new T3 app. Give your project a name, select Next.js, TypeScript, tRPC, NextAuth, Prisma, and Tailwind CSS when prompted.

Once the setup is complete, navigate to the project folder and open it in your code editor. The T3 CLI generates a well-structured project with sensible defaults and all the core integrations in place. Take a moment to familiarize yourself with the file structure.

The key files and directories to note are:

  • pages/: Contains all the Next.js pages that make up the app
  • server/: Contains the tRPC API routes and utility functions
  • prisma/: Contains the Prisma schema and migration files
  • public/: Contains static assets like images
  • styles/: Contains the global CSS and Tailwind configuration

With the basic project structure in place, we‘re ready to start building out our YouTube clone!

Designing the Database Schema

Let‘s start by designing the database schema for our app using Prisma. Prisma uses a declarative data modeling language to define the shape of the database. We‘ll create models to represent the core entities in our app: User, Video, Comment, Like, etc.

Open the prisma/schema.prisma file and add the following models:

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  videos        Video[]
  likes         Like[]
  comments      Comment[]
}

model Video {
  id          String    @id @default(cuid())
  title       String
  description String
  url         String
  thumbnail   String
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  authorId    String
  author      User      @relation(fields: [authorId], references: [id])
  likes       Like[]
  comments    Comment[]
}

model Comment {
  id        String   @id @default(cuid())
  text      String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  videoId   String
  video     Video    @relation(fields: [videoId], references: [id])
  authorId  String
  author    User     @relation(fields: [authorId], references: [id])
}

model Like {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  videoId   String
  video     Video    @relation(fields: [videoId], references: [id])
  userId    String
  user      User     @relation(fields: [userId], references: [id])
}

This schema defines the core entities we need to build our app, with fields to capture the relevant data and relationships between them. The User model represents registered users, with fields for their details and relations to the videos, likes, and comments they create. The Video model represents uploaded videos, with fields for the title, description, original URL, and a reference to the user who uploaded it, along with relations to associated likes and comments. The Comment and Like models enable users to interact with videos.

With the schema defined, we can run a Prisma migration to create the corresponding tables in the database:

npx prisma migrate dev --name init

Prisma will prompt you to enter a name for the migration, generate the migration files, and apply them to the database. Our database is now set up and ready to use!

For development purposes, it‘s useful to seed the database with some sample data. Create a new file called prisma/seed.ts and add the following code:

import { PrismaClient } from ‘@prisma/client‘

const prisma = new PrismaClient()

async function main() {
  // Create sample users
  const alice = await prisma.user.create({
    data: {
      name: ‘Alice‘,
      email: ‘[email protected]‘,
    },
  })

  const bob = await prisma.user.create({
    data: {
      name: ‘Bob‘,
      email: ‘[email protected]‘,
    },
  })

  // Create sample videos
  const video1 = await prisma.video.create({
    data: {
      title: ‘My First Video‘,
      description: ‘This is my first video on the platform!‘,
      url: ‘https://example.com/video1.mp4‘, 
      thumbnail: ‘/thumbnails/video1.jpg‘,
      author: {
        connect: { id: alice.id },
      },
    },
  })

  const video2 = await prisma.video.create({
    data: {
      title: ‘Another Great Video‘,
      description: ‘Yet another amazing video‘,
      url: ‘https://example.com/video2.mp4‘,
      thumbnail: ‘/thumbnails/video2.jpg‘,  
      author: {
        connect: { id: bob.id },
      },
    },
  })

  // Create sample comments
  await prisma.comment.create({
    data: {
      text: ‘Great video!‘,
      video: {
        connect: { id: video1.id },
      },
      author: {
        connect: { id: bob.id },
      },
    },  
  })

  await prisma.comment.create({
    data: {
      text: ‘Thanks, glad you liked it!‘,
      video: {
        connect: { id: video1.id },
      },
      author: {  
        connect: { id: alice.id },
      },
    },
  })
}

main()
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

This script creates some sample users, videos, and comments using Prisma‘s fluent API for seeding relational data. To run it, add the following script to package.json:

{
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  },
  "scripts": {
    "seed": "prisma db seed"
  }
}

Then run the seed script:

npm run seed

Our database now contains realistic sample data we can use to build out the UI.

Building out the Core Pages and Components

With our database ready, let‘s move on to building the core pages and components of our YouTube clone using Next.js and Tailwind CSS.

First, update the homepage to display a grid of videos pulled from the database. Open pages/index.tsx and replace the contents with:

import { Video } from ‘@prisma/client‘
import { GetServerSideProps } from ‘next‘
import prisma from ‘lib/prisma‘
import VideoCard from ‘components/VideoCard‘

export const getServerSideProps: GetServerSideProps = async () => {
  const videos = await prisma.video.findMany({
    include: {
      author: true,
    },
    orderBy: {
      createdAt: ‘desc‘,
    }
  })

  return {
    props: { videos },
  }
}

type HomeProps = {
  videos: (Video & {
    author: {
      id: string
      name: string | null
    }
  })[]
}

export default function Home({ videos }: HomeProps) {
  return (
    <div>
      <h1 className="text-4xl font-bold mb-8">Featured Videos</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        {videos.map((video) => (
          <VideoCard key={video.id} video={video} />  
        ))}
      </div>
    </div>
  )
}

This code uses Next.js‘ getServerSideProps to fetch the list of videos from the database on the server and pass them to the Home component as props. The Home component then renders a grid of VideoCard components, passing each video as a prop.

Create a new file components/VideoCard.tsx and add the following code:

import { Video } from ‘@prisma/client‘
import Image from ‘next/image‘
import Link from ‘next/link‘

type VideoCardProps = {
  video: Video & {
    author: {
      id: string
      name: string | null
    }
  }
}

export default function VideoCard({ video }: VideoCardProps) {
  return (
    <Link href={`/video/${video.id}`}>
      <div className="bg-white shadow-md rounded-md overflow-hidden">
        <Image 
          src={video.thumbnail}
          alt={video.title}
          width={320}
          height={180}
        />
        <div className="p-4">
          <h2 className="text-lg font-semibold">{video.title}</h2>
          <p className="text-gray-500 text-sm mt-1">{video.author.name}</p> 
        </div>
      </div>  
    </Link>
  )
}

This component renders a card for each video, displaying its thumbnail, title, and author name. The card links to the video‘s dedicated page.

Next, create a new file pages/video/[id].tsx for the video page:

import { Video } from ‘@prisma/client‘
import { GetServerSideProps } from ‘next‘
import prisma from ‘lib/prisma‘
import Link from ‘next/link‘

export const getServerSideProps: GetServerSideProps = async (context) => {
  const video = await prisma.video.findUnique({
    where: {
      id: context.params?.id as string
    },
    include: {
      author: true
    }
  })

  return {
    props: { video },
  }
}

type VideoPageProps = {
  video: Video & {
    author: {
      id: string
      name: string | null
    }  
  }
}

export default function VideoPage({ video }: VideoPageProps) {
  return (
    <div>
      <h1 className="text-4xl font-bold mb-4">{video.title}</h1>
      <div className="aspect-w-16 aspect-h-9">
        <iframe
          src={video.url}
          allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
          allowFullScreen
        ></iframe>
      </div>
      <p className="text-gray-500 text-sm mt-1">Uploaded by <Link href={`/profile/${video.author.id}`}>{video.author.name}</Link></p>
      <p className="mt-4">{video.description}</p>
    </div>
  )  
}

This page displays the details for an individual video, including the title, embedded video player, author link, and description. It fetches the video data from the database using getServerSideProps based on the id parameter in the URL.

These are just a few of the core pages and components needed for a YouTube clone. You‘d also want to add pages for search results, user profiles, playlists, and so on. But this should give you a good starting point to understand how the pieces fit together!

The final step is to deploy the application to a hosting platform like Vercel. Create a new Vercel project and connect it to your GitHub repository. Vercel will automatically detect the Next.js app and deploy it. Be sure to set up the necessary environment variables in the Vercel dashboard for the database connection.

Conclusion

Congratulations, you now have a working YouTube clone built with the T3 stack! In this post, we covered how to:

  • Set up a new T3 project with Next.js, TypeScript, tRPC, NextAuth, Prisma, and Tailwind CSS
  • Design a database schema using Prisma
  • Seed the database with sample data
  • Build pages and components using Next.js and Tailwind CSS
  • Deploy the application to Vercel

Of course, there are many more features you could add, such as user authentication, commenting, liking videos, playlists, and an admin dashboard. Now that you understand the core concepts, you can use the T3 stack to build out these features and flesh out your YouTube clone even further.

The T3 stack provides an incredibly powerful, productive, and enjoyable way to build full-stack TypeScript applications. By leveraging these tools, you can build scalable, maintainable applications in record time. I hope this post has inspired you to give it a try and level up your skills. Happy coding!

Similar Posts