Gần đây trên nhiều kênh youtube mình hay theo dõi về web developer thì thấy xuất hiện ngày càng nhiều project được làm với T3 stack (Nextjs, Tailwind, Prisma,...) Vậy T3 stack là gì? Nó có gì hot?

Đến thời điểm hiện tại thì mình cũng mới chỉ research một chút về T3 stack thôi nên cũng chưa có kiến thức thực tế nào về các dự án làm từ T3 stack. Vậy anh em cùng mình đi tìm hiểu xem nó là gì nhé :))


Create T3 App

Cứ follow doc của t3-stack ở đây mà làm thôi: Installation • Create T3 App

npm create t3-app@latest

Sau đó enter select các option mà mình muốn, mình thì sẽ chọn full options luôn để xem nó như thế nào: Typescript, NextAuth, Tailwind, Prisma, Trpc.

Đợi nó install hết các package cần thiết xong thì chúng ta đã có source code rồi.

Thử run app lên xem thế nào bằng npm run dev nhưng của mình lại bị lỗi luôn thế này:

SyntaxError: Unexpected token '??='

Search google thì là do node version phải từ bản 18 đổ lên. Thế thì mình sẽ dùng nvm cài hẳn bản node v20.2.0 mới nhất bây giờ luôn :v

Run lại app cái thì ăn luôn =))



Connect to database

Anh em thấy t3-stack có dùng thằng Prisma (optional) thì theo tìm hiểu của mình và hỏi ChatGPT :

Prisma được thiết kế để làm việc với nhiều hệ quản trị cơ sở dữ liệu phổ biến như PostgreSQL, MySQL, SQLite và SQL Server. Nó cho phép bạn xác định cấu trúc cơ sở dữ liệu bằng cách sử dụng ngôn ngữ Prisma Schema, sau đó tạo ra mã TypeScript hoặc JavaScript để tạo, đọc, cập nhật và xóa dữ liệu trong cơ sở dữ liệu.Prisma sử dụng một ngôn ngữ riêng gọi là Prisma Query Language (PQL) để tạo và thực thi truy vấn cơ sở dữ liệu. PQL là một ngôn ngữ độc lập với hệ quản trị cơ sở dữ liệu, giúp bạn tạo ra các truy vấn đồng nhất cho nhiều loại cơ sở dữ liệu khác nhau. Điều này có nghĩa là bạn có thể sử dụng cùng một cú pháp để thực hiện các truy vấn dù bạn đang làm việc với PostgreSQL, MySQL, SQLite hay SQL Server.

Vậy thằng Prisma này giải quyết được vấn đề là anh em làm việc với mỗi loại database thì đều phải hỏi các cú pháp query của nó thì giờ chỉ cần biết syntax của PQL thồi là đỡ phải nhớ nhiều rồi. Còn nhiều nguyên nhân lý do sâu xa nên chọn Prisma thì anh em đọc ở trang chủ của nó: Why Prisma? Comparison with SQL query builders & ORMs


Mình thấy default thì database là thằng PostgreSQL nhưng trước giờ mình dùng Mongodb free cũng quen thuộc hơn nên mình sẽ chọn Mongodb để dùng với Prisma nhé. Hướng dẫn setup thì anh em xem ở đây: Connect your database | typescript-mongodb (prisma.io)


Mình sẽ thay connection string của mình vào DATABASE_URL trong .env file và update file schema.prisma ở folder prisma như sau:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["mongodb"]
}


datasource db {
  provider = "mongodb"
  // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
  // Further reading:
  // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
  // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
  url      = env("DATABASE_URL")
}


model Example {
  id        String   @id @default(auto()) @map("_id") @db.ObjectId
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}


// Necessary for Next auth
model Account {
  id                String  @id @default(auto()) @map("_id") @db.ObjectId
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? // @db.Text
  access_token      String? // @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? // @db.Text
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)


  @@unique([provider, providerAccountId])
}


model Session {
  id           String   @id @default(auto()) @map("_id") @db.ObjectId
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}


model User {
  id            String    @id @default(auto()) @map("_id") @db.ObjectId
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}


model VerificationToken {
  id         String   @id @default(auto()) @map("_id") @db.ObjectId
  identifier String
  token      String   @unique
  expires    DateTime


  @@unique([identifier, token])
}

Để tiện trong việc dễ dàng đọc file .prisma thì anh em nên cài extenstion tên là: Prisma luôn nhé. Nó highlight với báo lỗi các thứ cho luôn =))


Sau đó anh em run lệnh npx prisma db push để push các model schema lên database:

npx prisma db push

Rồi run lệnh npx prisma studio rồi mở link local này http://localhost:5555/ để xem được trực quan database luôn khá là tiện nhé.

npx prisma studio


Authentication với NextAuth

Mặc định auth của app là Discord, anh em chỉ cần add clientId và secret key từ Discord vào file .env là được. Nhưng mình thì muốn auth với Github hơn vì đa số anh em dev thì đều có account Github mà. Hoặc anh em có thể dùng cả 2, với mình thì chỉ với Github thôi.

Trước hết anh em đăng nhập vào github bằng account của mình > Settings > Developer settings > OAuth apps > tạo new OAuth app > Điền thông tin phù hợp với anh em như bên dưới của mình:

Sau đó bấm Register application và lấy được client ID và client secret key.

Rồi anh em thay thế các file mà dùng DISCORD_CLIENT_ID và DISCORD_CLIENT_SECRET thành Github:

/** env.mjs file  */
import { z } from "zod";
import { createEnv } from "@t3-oss/env-nextjs";


export const env = createEnv({
  /**
   * Specify your server-side environment variables schema here. This way you can ensure the app
   * isn't built with invalid env vars.
   */
  server: {
    DATABASE_URL: z.string().url(),
    NODE_ENV: z.enum(["development", "test", "production"]),
    NEXTAUTH_SECRET:
      process.env.NODE_ENV === "production"
        ? z.string().min(1)
        : z.string().min(1).optional(),
    NEXTAUTH_URL: z.preprocess(
      // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
      // Since NextAuth.js automatically uses the VERCEL_URL if present.
      (str) => process.env.VERCEL_URL ?? str,
      // VERCEL_URL doesn't include `https` so it cant be validated as a URL
      process.env.VERCEL ? z.string().min(1) : z.string().url(),
    ),
    // Add `.min(1) on ID and SECRET if you want to make sure they're not empty
    GITHUB_CLIENT_ID: z.string(),
    GITHUB_CLIENT_SECRET: z.string(),
  },

/**
   * Specify your client-side environment variables schema here. This way you can ensure the app
   * isn't built with invalid env vars. To expose them to the client, prefix them with
   * `NEXT_PUBLIC_`.
   */
  client: {
    // NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
  },

  /**
   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
   * middlewares) or client-side so we need to destruct manually.
   */
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    NODE_ENV: process.env.NODE_ENV,
    NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
    NEXTAUTH_URL: process.env.NEXTAUTH_URL,
    GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
    GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
  },
});



/** auth.ts */
import { type GetServerSidePropsContext } from "next";
import {
  getServerSession,
  type NextAuthOptions,
  type DefaultSession,
} from "next-auth";
import GithubProvider from "next-auth/providers/github";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { env } from "~/env.mjs";
import { prisma } from "~/server/db";


/**
 * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
 * object and keep type safety.
 *
 * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
 */
declare module "next-auth" {
  interface Session extends DefaultSession {
    user: {
      id: string;
      // ...other properties
      // role: UserRole;
    } & DefaultSession["user"];
  }


  // interface User {
  //   // ...other properties
  //   // role: UserRole;
  // }
}


/**
 * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
 *
 * @see https://next-auth.js.org/configuration/options
 */
export const authOptions: NextAuthOptions = {
  callbacks: {
    session: ({ session, user }) => ({
      ...session,
      user: {
        ...session.user,
        id: user.id,
      },
    }),
  },
  adapter: PrismaAdapter(prisma),
  providers: [
    GithubProvider({
      clientId: env.GITHUB_CLIENT_ID,
      clientSecret: env.GITHUB_CLIENT_SECRET,
    }),
    /**
     * ...add more providers here.
     */
  ],
};


/**
 * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
 *
 * @see https://next-auth.js.org/configuration/nextjs
 */
export const getServerAuthSession = (ctx: {
  req: GetServerSidePropsContext["req"];
  res: GetServerSidePropsContext["res"];
}) => {
  return getServerSession(ctx.req, ctx.res, authOptions);
};


# .env file
DATABASE_URL="file:./db.sqlite"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# or with node: crypto.randomUUID()
# https://next-auth.js.org/configuration/options#secret
NEXTAUTH_SECRET="2c309200-d0e7-4e6c-9e4d-9b3fe9b51356"
NEXTAUTH_URL="http://localhost:3000"


# Next Auth Github Provider
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""


Thế là xong rồi, rất là đơn giản đúng không. Nếu anh em muốn tìm hiểu thêm như login với google, facebook,... thì có thể xem thêm ở trang chủ của NextAuth: Getting Started | NextAuth.js (next-auth.js.org)

Sau đó anh em nhớ khởi động lại app và run lại với: npm run dev là done


Tạo Todo page UI

Mình sẽ tạo một trang Todo để show list việc cần làm là Todo component, mỗi việc sẽ là 1 component tên là TodoItem css thì dĩ nhiên là dùng tailwind css rồi cho nó nhanh.


// src/pages/todo.tsx
import { type NextPage } from "next";
		
import Link from "next/link";
			
const Todo: NextPage = () => {
		
  return (
    <div className="h-screen w-screen bg-gray-100 p-4">
		
      <Link
		
        href="/"
		
        className="inline-flex items-center rounded-md bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-600"
		
      >
		
        <svg
		
          xmlns="http://www.w3.org/2000/svg"
		
          className="mr-2 h-5 w-5"
		
          fill="none"
		
          viewBox="0 0 24 24"
		
          stroke="currentColor"
		
        >
		
          <path
		
            stroke-linecap="round"
		
            stroke-linejoin="round"
		
            stroke-width="2"
		
            d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
		
          />
		
        </svg>
		
        Back to home page
		
      </Link>
		
      <div className="mt-10 flex items-center justify-center font-medium">
		
        <div className="flex h-full flex-grow items-center justify-center  text-gray-600">
		
          <div className="w-96 max-w-full rounded-lg bg-white p-8 shadow-lg">
		
            <div className="mb-6 flex items-center">
		
              <svg
		
                className="h-8 w-8 stroke-current text-indigo-500"
		
                xmlns="http://www.w3.org/2000/svg"
		
                fill="none"
		
                viewBox="0 0 24 24"
		
                stroke="currentColor"
		
              >
		
                <path
		
                  stroke-linecap="round"
		
                  stroke-linejoin="round"
		
                  stroke-width="2"
		
                  d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
		
                />
		
              </svg>
		
              <h4 className="ml-3 text-lg font-semibold">Todo List</h4>
		
            </div>
		
            <TodoItem name="nau com" id={"1"} complete={false} />
		
            <TodoItem name="rau bat" id={"2"} complete={false} />
		
            <TodoItem name="giat quan ao" id={"3"} complete={false} />
		
            <TodoItem name="learning English 1h" id={"4"} complete={false} />
		
            <TodoItem name="ve sinh ca nhan" id={"1"} complete={true} />
		
            <div className="mt-4 flex items-center justify-between gap-2">
		
              <input
		
                className="ml-4 h-8 w-full flex-grow bg-transparent font-medium focus:outline-none"
		
                type="text"
		
                placeholder="add a new task"
		
              />
		
              <button
		
                type="button"
		
                className="mr-2 inline-flex items-center rounded-full bg-blue-700 p-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
		
              >
		
                <svg
		
                  className="h-5 w-5 fill-current text-white"
		
                  xmlns="http://www.w3.org/2000/svg"
		
                  fill="none"
		
                  viewBox="0 0 24 24"
		
                  stroke="currentColor"
		
                >
		
                  <path
		
                    stroke-linecap="round"
		
                    stroke-linejoin="round"
		
                    stroke-width="2"
		
                    d="M12 6v6m0 0v6m0-6h6m-6 0H6"
		
                  />
		
                </svg>
		
                <span className="sr-only">add button</span>
		
              </button>
		
            </div>
		
          </div>
		
        </div>
		
      </div>
		
    </div>
		
  );
		
};
		


		
const TodoItem: NextPage<{
		
  name: string;
		
  complete: boolean;
		
  id: string;
		
}> = ({ name, complete, id }) => {
		
  return (
		
    <div className="flex items-center justify-between">
		
      <div>
		
        <input className="hidden" type="checkbox" id={id} checked={complete} />
		
        <label
		
          className="flex h-10 cursor-pointer items-center rounded px-2 hover:bg-gray-100"
		
          htmlFor="task_5"
		
        >
		
          <span className="flex h-5 w-5 items-center justify-center rounded-full border-2 border-gray-300 text-transparent">
		
            <svg
		
              className="h-4 w-4 fill-current"
		
              xmlns="http://www.w3.org/2000/svg"
		
              viewBox="0 0 20 20"
		
              fill="currentColor"
		
            >
		
              <path
		
                fill-rule="evenodd"
		
                d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
		
                clip-rule="evenodd"
		
              />
		
            </svg>
		
          </span>
		
          <span className="ml-4 text-sm">{name}</span>
		
        </label>
		
      </div>
		
      <button
		
        type="button"
		
        className="font-small rounded-lg bg-transparent px-5 py-2.5 text-sm text-red-500  hover:text-red-600"
		
      >
		
        Delete
		
      </button>
		
    </div>
		
  );
		
};
		
export default Todo;


Tuy đã dùng tailwind rồi nhưng đôi khi anh em vẫn cần phải style hay custom, mình sẽ tạo thêm 1 file css để style cho trang Todo này và import nó vào file css global nhé.

/* src/styles/todo.css */
input[type="checkbox"]:checked + label span:first-of-type {	
  background-color: #10b981;	
  border-color: #10b981;	
  color: #fff;	
}
			
input[type="checkbox"]:checked + label span:nth-of-type(2) {
  text-decoration: line-through;
  color: #9ca3af;		
}


/* src/styles/globals.css */
@tailwind base;	
@tailwind components;	
@tailwind utilities;
	
		
@import "todo";


Vậy là chúng ta đã có trang todo rồi nhưng sẽ cần phải add nó vào trang home page. Mình sẽ sửa trang index (home page) 1 chút như bên dưới.

// src/pages/index.tsx
import { type NextPage } from "next";
import { signIn, signOut, useSession } from "next-auth/react";
import Head from "next/head";
import Link from "next/link";


const Home: NextPage = () => {
  return (
    <>
      <Head>
        <title>Create T3 App</title>
        <meta name="description" content="Generated by create-t3-app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c]">
        <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 ">
          <h1 className="text-5xl font-extrabold tracking-tight text-white sm:text-[5rem]">
            Create <span className="text-[hsl(280,100%,70%)]">T3</span> App
          </h1>
          <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
            <Link
              className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20"
              href="https://create.t3.gg/en/introduction"
              target="_blank"
            >
              <h3 className="text-2xl font-bold">Documentation →</h3>
              <div className="text-lg">
                Learn more about Create T3 App, the libraries it uses, and how
                to deploy it.
              </div>
            </Link>
            <Link
              className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20"
              href="/todo"
            >
              <h3 className="text-2xl font-bold">Todo List →</h3>
              <div className="text-lg">
                Go to this page to see what you have to do today
              </div>
            </Link>
          </div>
          <div className="flex flex-col items-center gap-2">
            <AuthShowcase />
          </div>
        </div>
      </main>
    </>
  );
};


export default Home;


const AuthShowcase: React.FC = () => {
  const { data: sessionData } = useSession();


  return (
    <div className="flex flex-col items-center justify-center gap-4">
      <p className="text-center text-2xl text-white">
        {sessionData && <span>Logged in as {sessionData.user?.name}</span>}
      </p>
      <button
        className="rounded-full bg-white/10 px-10 py-3 font-semibold text-white no-underline transition hover:bg-white/20"
        onClick={
          sessionData
            ? () => void signOut()
            : () => void signIn("github", { callbackUrl: "/todo" })
        }
      >
        {sessionData ? "Sign out" : "Sign in"}
      </button>
    </div>
  );
};



Giờ chúng ta có trang home page thế này =))



Còn trang todo sẽ thế này:


Config Private Route

Mình đã tạo xong UI cho trang Todo, giờ mình muốn chỉ khi login rồi thì mới có thể vào được trang todo thì sẽ làm thế nào?

Qua nghiên cứu đọc doc bên Nextjs thì từ version Nextjs 12 thì có thêm feature gọi là middleware, nó có thể làm rất nhiều thứ hay ho như: Set headers, using cookies, Response,... đặc biệt Matching paths có thể giúp anh em làm được private route chỉ với vài dòng config trong 1 file là middleware.ts cùng cấp với thư mục pages như sau:

// src/middleware.ts
export { default } from "next-auth/middleware"

export const config = { matcher: ["/todo"] }

Những route nào anh em muốn private thì put nó vào list matcher là được. Nhưng khi mình config như trên thì gặp 1 số lỗi như login xong thì không back về trang callback, hoặc click vào trang Todo khi chưa login thì vẫn được??? Sau một hồi tìm hiểu thì mình sửa lại file auth.ts như bên dưới thì được:


Tạo Todo APIs

Giờ chúng ta chỉ cần làm các api todo để integrate với phần UI bên trên là ok đúng không. Vậy cùng xem cách viết API trên Nextjs với prisma và TRPC nó thế nào nhé.

Anh em có thể đọc code api example để biết qua cách tạo API và sử dụng nó ở phía client thế nào. Cũng không có gì phức tạo, mình sẽ xóa file api example đi vào tạo file todo API gồm: add todo, get todo list, update todo status và delete todo như sau:


Trước hết mình sẽ xóa model Example vào thay vào đó là model cho Todo trong database:

// prisma/schema.prisma
model Todo {
  id        String   @id @default(auto()) @map("_id") @db.ObjectId
  name      String
  complete  Boolean
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}


Viết các api cho todo:

// src/server/api/routers/todo.ts
import { z } from "zod";
import {
  createTRPCRouter,
  publicProcedure,
  protectedProcedure,
} from "~/server/api/trpc";

export const todoRouter = createTRPCRouter({
  addTodo: publicProcedure
    .input(z.object({ name: z.string() }))
    .mutation(({ ctx, input }) => {
      return ctx.prisma.todo.create({
        data: {
          name: input.name,
          complete: false,
        },
      });
    }),

  todoList: publicProcedure.query(({ ctx }) => {
    return ctx.prisma.todo.findMany();
  }),

  deleteTodo: protectedProcedure
    .input(z.object({ id: z.string() }))
    .mutation(({ ctx, input }) => {
      return ctx.prisma.todo.delete({
        where: {
          id: input.id,
        },
      });
    }),

  updateTodoStatus: protectedProcedure
    .input(z.object({ id: z.string(), complete: z.boolean() }))
    .mutation(({ ctx, input }) => {
      console.log('id, complete', input)
      return ctx.prisma.todo.update({
        where: {
          id: input.id,
        },
        data: {
          complete: input.complete,
        },
      });
    }),
});

Add todo router vào file root router:

// src/server/api/root.ts
import { createTRPCRouter } from "~/server/api/trpc";
import { todoRouter } from "~/server/api/routers/todo";
/**
 * This is the primary router for your server.
 *
 * All routers added in /api/routers should be manually added here.
 */
export const appRouter = createTRPCRouter({
  todo: todoRouter,
});

// export type definition of API
export type AppRouter = typeof appRouter;


Và cuối cùng là áp dụng nó vào file UI Todo.tsx:

// src/pages/todo.tsx
import { type NextPage } from "next";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { api } from "~/utils/api";

const Todo: NextPage = () => {
  const [todoInput, setTodoInput] = useState<string>("");
  const { data: sessionData } = useSession();
  const { data: todos, refetch: refetchTodos } = api.todo.todoList.useQuery(
    undefined, // no input
    { enabled: sessionData?.user !== undefined }
  );
  const createTodo = api.todo.addTodo.useMutation({
    onSuccess: () => {
      void refetchTodos();
    },
  });

  const handleAddTodo = () => {
    createTodo.mutate({
      name: todoInput,
    });
    setTodoInput("");
  };


  return (
    <div className="h-screen w-screen bg-gray-100 p-4">
      <Link
        href="/"
        className="inline-flex items-center rounded-md bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-600"
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          className="mr-2 h-5 w-5"
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
        >
          <path
            stroke-linecap="round"
            stroke-linejoin="round"
            stroke-width="2"
            d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
          />
        </svg>
        Back to home page
      </Link>
      <div className="mt-10 flex items-center justify-center font-medium">
        <div className="flex h-full flex-grow items-center justify-center  text-gray-600">
          <div className="w-96 max-w-full rounded-lg bg-white p-8 shadow-lg">
            <div className="mb-6 flex items-center">
              <svg
                className="h-8 w-8 stroke-current text-indigo-500"
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-width="2"
                  d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
                />
              </svg>
              <h4 className="ml-3 text-lg font-semibold">Todo List</h4>
            </div>
            {todos &&
              todos.length > 0 &&
              todos.map((todo) => (
                <TodoItem
                  key={todo.id}
                  name={todo.name}
                  id={todo.id}
                  complete={todo.complete}
                />
              ))}
            <div className="mt-4 flex items-center justify-between gap-2">
              <input
                className="ml-4 h-8 w-full flex-grow bg-transparent font-medium focus:outline-none"
                type="text"
                value={todoInput}
                onChange={(e) => setTodoInput(e.target.value)}
                placeholder="add a new task"
              />
              <button
                onClick={handleAddTodo}
                disabled={!todoInput}
                type="button"
                className="mr-2 inline-flex items-center rounded-full bg-blue-700 p-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
              >
                <svg
                  className="h-5 w-5 fill-current text-white"
                  xmlns="http://www.w3.org/2000/svg"
                  fill="none"
                  viewBox="0 0 24 24"
                  stroke="currentColor"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M12 6v6m0 0v6m0-6h6m-6 0H6"
                  />
                </svg>
                <span className="sr-only">add button</span>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

const TodoItem: NextPage<{
  name: string;
  complete: boolean;
  id: string;
}> = ({ name, complete, id }) => {
  const [isDone, setIsDone] = useState<boolean>(complete);
  const [debouncedValue, setDebouncedValue] = useState<boolean>(complete);
  const { data: sessionData } = useSession();
  const { refetch: refetchTodos } = api.todo.todoList.useQuery(
    undefined, // no input
    { enabled: sessionData?.user !== undefined }
  );
  const deleteTodo = api.todo.deleteTodo.useMutation({
    onSuccess: () => {
      void refetchTodos();
    },
  });
  const updateTodoStatus = api.todo.updateTodoStatus.useMutation({
    onSuccess: () => {
      void refetchTodos();
    },
  });

  const handelDeleteTodo = () => {
    deleteTodo.mutate({
      id: id,
    });
  };

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(isDone);
    }, 500);


    return () => {
      clearTimeout(handler);
    };
  }, [isDone]);


  useEffect(() => {
    updateTodoStatus.mutate({
      id: id,
      complete: debouncedValue,
    });
  }, [debouncedValue]);


  return (
    <div className="flex items-center justify-between">
      <div onClick={() => setIsDone((prev) => !prev)}>
        <input
          className="hidden"
          type="checkbox"
          id={id}
          checked={isDone}
          onChange={() => setIsDone((prev) => !prev)}
        />
        <label
          className="flex h-10 cursor-pointer items-center rounded px-2 hover:bg-gray-100"
          htmlFor="task_5"
        >
          <span className="flex h-5 w-5 items-center justify-center rounded-full border-2 border-gray-300 text-transparent">
            <svg
              className="h-4 w-4 fill-current"
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 20 20"
              fill="currentColor"
            >
              <path
                fill-rule="evenodd"
                d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
                clip-rule="evenodd"
              />
            </svg>
          </span>
          <span className="ml-4 text-sm">{name}</span>
        </label>
      </div>
      <button
        onClick={handelDeleteTodo}
        type="button"
        className="font-small rounded-lg bg-transparent px-5 py-2.5 text-sm text-red-500  hover:text-red-600"
      >
        Delete
      </button>
    </div>
  );
};

export default Todo;


Deploy app

Để deploy Nextjs app thì mình nghĩ nên dùng vercel để deploy vì nó free và rất nhanh, dùng đồ của nó để nướng nó thì là ngon nhất rồi =))

Push code của anh em lên github của mình rồi vào trang Dashboard – Vercel đăng nhập với account github. Tạo new project => chọn project từ github => giữ nguyên default setup và kéo xuống phần enviroments rồi add tất cả các enviroments trong file .env vào => Deploy

Sau khi deploy thành công thì anh em lấy url hiện tại của app để gắn lại vào Enviroment NEXTAUTH_URL thay vì là local như trước để khi login không bị lỗi nhé. Add xong thì anh em bấm redeploy lại là được nhé.

Thành quả sau khi deploy của mình đây: Create T3 App (t3-stack-learning.vercel.app) <3

Link repo: Gnutyud/t3-stack-learning (github.com)


Túm lại

Như vậy sau khi cùng anh em đi tìm hiểu rốt cuộc T3 stack nó là gì thì như anhh em thấy đấy, nó thực chất không phải 1 một công nghệ mới hay gì cả. T3 stack thực chất là do 1 ông anh tên Theo xây dựng lên như một starter toolkit dùng các công nghệ có sẵn như là: Nextjs, NextAuth, Typecsript, Tailwind, TRPC,... dùng để tạo ra 1 con app fullstack dựa vào Nextjs. Tuy nhiên điểm hay ho ở đây là anh ta chọn các công nghệ trên kết hợp với nhau mọi thứ đều typesafe, giúp anh em code đỡ rác hơn do có typescript nó chửi cho liên tục =))

Theo cảm nhận cá nhân mình thì thấy nó cũng khá là hay ở khoản dùng thằng Prisma và TRPC để code phần API trông nó đơn

giản hơn rất nhiều. Còn thực ra anh em hoàn toàn có thể dùng Nextjs để code fullstack được chỉ là đã có người chọn ra giúp mình các thư viện hay ho để code cùng Nextjs rồi thì mình đỡ phải đi mò tìm các thư viện để làm thôi.