Next.js: Projeto Final: Autenticação & Configuração

Última atualização: 2026-08-26

Construir um projeto SaaS completo é como construir um edifício — primeiro, você usa andaimes para erguer a estrutura; depois, projeta os sistemas elétrico e hidráulico (o banco de dados); e, finalmente, instala as fechaduras das portas (o sistema de autenticação).

1. O Que Você Vai Aprender



2. A História Real de um Engenheiro Full-Stack

(1) Ponto de Dor: O Início Caótico de Construir uma Plataforma SaaS do Zero

Alice é engenheira full-stack em uma startup de 50 pessoas. A empresa decidiu desenvolver uma plataforma interna de colaboração de projetos chamada "TaskFlow" para atender 10.000 usuários, cada um gerenciando mais de 200 tarefas. Alice tentou configurar a estrutura do projeto manualmente — mas o roteamento virou uma bagunça, as migrações de banco de dados falhavam frequentemente e todas as três soluções de autenticação que ela propôs foram rejeitadas. A equipe estava perdendo quatro horas por dia em configuração de ambiente e o projeto estava duas semanas atrasado.

(2) Solução Usando Scaffolding + Framework de Autenticação

Inicialização com um clique usando create-next-app + modelagem declarativa com Prisma ORM + Auth.js pronto para uso.

BASH
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
npx prisma init --datasource-provider postgresql
npx auth add

Três linhas de código resolvem 80% do trabalho de inicialização.

(3) Resultados

Dimensão Configuração Manual Solução TaskFlow
Tempo de Inicialização 2 dias 2 horas
Desenvolvimento da Solução de Autenticação 1 semana (JWT próprio) 30 minutos (integração Auth.js)
Migração de Banco de Dados Scripts SQL Manuais Prisma Migrate (Declarativo)
Diretrizes de Estrutura do Projeto Cada equipe com seu estilo App Router: Roteamento Convencional
Manutenibilidade Baixa (sem type safety) Alta (TypeScript + tipos Prisma)


3. Inicialização e Configuração do Projeto

(1) Escolhendo a ferramenta de scaffolding create-next-app

100%
graph LR
    A[create-next-app] --> B[TypeScript]
    A --> C[Tailwind CSS]
    A --> D[ESLint]
    A --> E[App Router]
    A --> F[Diretório src/]
    A --> G[Alias @/]

    style A fill:#cce5ff
    style B fill:#d4edda
    style C fill:#d4edda
Opção Valor Descrição
TypeScript Sim Type safety — obrigatório para produção
ESLint Sim Padrões de Codificação
Tailwind CSS Sim Funciona com shadcn/ui
Diretório src/ Sim Separado de app/ para uma estrutura clara
App Router Sim Padrão do Next.js 16
alias de import @/* caminho de import conciso

▶ Exemplo: Inicializando um Projeto TaskFlow

Saída:

TEXT 📖 Somente leitura
Diagrama do fluxo de dados e estrutura de componentes.
BASH
# 1. Criar um Projeto
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"

# 2. Ir para o Diretório
cd taskflow

# 3. Instalar shadcn/ui
npx shadcn@latest init -d

# 4. Adicionar Componentes Comuns do shadcn/ui
npx shadcn@latest add button card input label select dialog dropdown-menu avatar badge separator

# 5. Iniciar o servidor de desenvolvimento
npm run dev
TEXT 📖 Somente leitura
✔ Projeto criado em: taskflow/
✔ shadcn/ui inicializado com sucesso
✔ Componente button instalado
✔ Componente card instalado
✔ Componente input instalado
✔ 7 componentes instalados no total

Saída:

TEXT 📖 Somente leitura
✔ Projeto criado em: taskflow/
✔ shadcn/ui inicializado com sucesso
✔ Componente button instalado
✔ Componente card instalado
✔ Componente input instalado
✔ 7 componentes instalados no total

(2) Estrutura de Diretórios do Projeto

TEXT 📖 Somente leitura
taskflow/
├── src/
│   ├── app/                    # Páginas do App Router
│   │   ├── (auth)/            # Grupo de Rotas Relacionadas à Autenticação
│   │   ├── (dashboard)/       # Grupo de Rotas do Dashboard
│   │   ├── api/               # Rotas de API
│   │   ├── layout.tsx         # Layout Raiz
│   │   └── page.tsx           # Página Inicial
│   ├── components/            # Componentes Compartilhados
│   │   ├── ui/               # Componentes shadcn/ui
│   │   └── forms/            # Componentes de Formulário
│   ├── lib/                  # Funções Utilitárias
│   │   ├── auth.ts           # Configuração Auth.js
│   │   ├── db.ts             # Prisma Client
│   │   └── utils.ts          # Ferramentas de Uso Geral
│   ├── prisma/               # Prisma Schema + Migração
│   │   ├── schema.prisma
│   │   └── seed.ts
│   └── middleware.ts         # Middleware de Proteção de Rotas
├── public/                   # Recursos Estáticos
├── next.config.ts            # Configuração Next.js
├── tailwind.config.ts        # Configuração Tailwind
└── package.json


4. Design dos Cinco Modelos do Prisma Schema

(1) Diagrama de Relacionamento do Modelo de Dados

100%
graph TB
    U[User] -->|pertence a| O[Organization]
    O -->|tem muitos| P[Project]
    P -->|tem muitas| T[Task]
    T -->|tem muitos| C[Comment]
    U -->|atribuído a| T
    U -->|criado por| C
    U -->|criado por| P

    style U fill:#cce5ff
    style O fill:#d4edda
    style P fill:#ffeeba
    style T fill:#f8d7da
    style C fill:#d6d8db

(2) Definindo Tipos de Enumeração

Nome do Enum Valor Descrição
Role OWNER / ADMIN / MEMBER / VIEWER Papéis Organizacionais
ProjectStatus ACTIVE / ARCHIVED / COMPLETED Status do Projeto
TaskStatus TODO / IN_PROGRESS / DONE Status da Tarefa
TaskPriority LOW / MEDIUM / HIGH / URGENT Prioridade da Tarefa

(3) Diagrama Prisma Completo

PRISMA
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum Role {
  OWNER
  ADMIN
  MEMBER
  VIEWER
}

enum ProjectStatus {
  ACTIVE
  ARCHIVED
  COMPLETED
}

enum TaskStatus {
  TODO
  IN_PROGRESS
  DONE
}

enum TaskPriority {
  LOW
  MEDIUM
  HIGH
  URGENT
}

model User {
  id             String   @id @default(cuid())
  name           String?
  email          String   @unique
  emailVerified  DateTime?
  image          String?
  passwordHash   String?
  organizationId String?
  organization   Organization? @relation(fields: [organizationId], references: [id])
  role           Role     @default(MEMBER)
  projects       Project[]
  assignedTasks  Task[]   @relation("TaskAssignee")
  comments       Comment[]
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

  accounts  Account[]
  sessions  Session[]
}

model Organization {
  id        String    @id @default(cuid())
  name      String
  slug      String    @unique
  inviteCode String   @unique @default(cuid())
  logo      String?
  users     User[]
  projects  Project[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Project {
  id          String        @id @default(cuid())
  name        String
  description String?
  status      ProjectStatus @default(ACTIVE)
  organizationId String
  organization Organization @relation(fields: [organizationId], references: [id])
  creatorId   String
  creator     User          @relation(fields: [creatorId], references: [id])
  tasks       Task[]
  color       String        @default("#3b82f6")
  createdAt   DateTime      @default(now())
  updatedAt   DateTime      @updatedAt
}

model Task {
  id          String       @id @default(cuid())
  title       String
  description String?
  status      TaskStatus   @default(TODO)
  priority    TaskPriority @default(MEDIUM)
  projectId   String
  project     Project      @relation(fields: [projectId], references: [id])
  assigneeId  String?
  assignee    User?        @relation("TaskAssignee", fields: [assigneeId], references: [id])
  creatorId   String
  creator     User         @relation(fields: [creatorId], references: [id])
  order       Int          @default(0)
  dueDate     DateTime?
  attachments Attachment[]
  comments    Comment[]
  createdAt   DateTime     @default(now())
  updatedAt   DateTime     @updatedAt
}

model Comment {
  id        String   @id @default(cuid())
  content   String
  taskId    String
  task      Task     @relation(fields: [taskId], references: [id])
  authorId  String
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Attachment {
  id        String   @id @default(cuid())
  fileName  String
  fileUrl   String
  fileSize  Int
  mimeType  String
  taskId    String
  task      Task     @relation(fields: [taskId], references: [id])
  uploaderId String
  uploader  User     @relation(fields: [uploaderId], references: [id])
  createdAt DateTime @default(now())
}

model Account {
  id                String  @id @default(cuid())
  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(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

▶ Exemplo: Executando uma migração de banco de dados

BASH
# 1. Criar uma Migração
npx prisma migrate dev --name init

# 2. Gerar Prisma Client
npx prisma generate

# 3. Visualizar Banco de Dados
npx prisma studio
TEXT 📖 Somente leitura
✔ Generated Prisma Client (v6.x) to .\node_modules\@prisma\client
✔ Your database is now in sync with your Prisma schema
✔ Prisma Studio opened at http://localhost:5555


5. Integração de Autenticação com Auth.js

(1) Comparação de Esquemas de Autenticação

Solução Vantagens Desvantagens Cenários Aplicáveis
Auth.js v5 Múltiplos provedores, sessões integradas, type-safe Curva de aprendizado moderada Projetos que exigem autenticação flexível
Clerk UI pronta para uso, sem configuração necessária Pago, vendor lock-in Prototipagem rápida
Supabase Auth Cota gratuita generosa, integrado com o banco de dados Requer integração com o ecossistema Supabase Solução totalmente gerenciada
JWT Desenvolvido Internamente Controle total Altos riscos de segurança, altos custos de desenvolvimento Não recomendado

(2) Configuração do Auth.js

TYPESCRIPT
// src/lib/auth.ts
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import Google from "next-auth/providers/google"
import { compare } from "bcryptjs"
import { prisma } from "./db"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    Credentials({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null
        }

        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        })

        if (!user || !user.passwordHash) {
          return null
        }

        const isValid = await compare(
          credentials.password as string,
          user.passwordHash
        )

        if (!isValid) {
          return null
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          image: user.image,
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
      }
      return token
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
      }
      return session
    },
  },
  pages: {
    signIn: "/auth/signin",
    error: "/auth/error",
  },
  session: {
    strategy: "jwt",
  },
})

▶ Exemplo: Rotas de API do Auth.js

TYPESCRIPT
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth"

export const { GET, POST } = handlers

Saída:

TEXT 📖 Somente leitura
Código TypeScript executado com sucesso.
TYPESCRIPT
// src/app/api/auth/signup/route.ts
import { NextResponse } from "next/server"
import { hash } from "bcryptjs"
import { prisma } from "@/lib/db"

export async function POST(request: Request) {
  try {
    const { name, email, password, inviteCode } = await request.json()

    const existingUser = await prisma.user.findUnique({
      where: { email },
    })

    if (existingUser) {
      return NextResponse.json(
        { error: "Email já cadastrado" },
        { status: 400 }
      )
    }

    const passwordHash = await hash(password, 12)

    let organizationId: string | null = null
    let role: "OWNER" | "ADMIN" | "MEMBER" | "VIEWER" = "MEMBER"

    if (inviteCode) {
      const org = await prisma.organization.findUnique({
        where: { inviteCode },
      })
      if (org) {
        organizationId = org.id
      }
    }

    const user = await prisma.user.create({
      data: {
        name,
        email,
        passwordHash,
        organizationId,
        role: organizationId ? "MEMBER" : "OWNER",
      },
    })

    if (!organizationId) {
      const org = await prisma.organization.create({
        data: {
          name: `Organização de ${name}`,
          slug: email.split("@")[0],
          users: { connect: { id: user.id } },
        },
      })
      await prisma.user.update({
        where: { id: user.id },
        data: { organizationId: org.id, role: "OWNER" },
      })
    }

    return NextResponse.json(
      { message: "Usuário criado com sucesso" },
      { status: 201 }
    )
  } catch (error) {
    return NextResponse.json(
      { error: "Algo deu errado" },
      { status: 500 }
    )
  }
}

(3) Sistema de Código de Convite da Organização

TYPESCRIPT
// src/lib/invite.ts
import { prisma } from "./db"

export async function generateInviteCode(organizationId: string) {
  const org = await prisma.organization.update({
    where: { id: organizationId },
    data: { inviteCode: crypto.randomUUID().slice(0, 8) },
  })
  return org.inviteCode
}

export async function validateInviteCode(code: string) {
  // Formato TKFLW-XXXXXX
  const fullCode = `TKFLW-${code.toUpperCase()}`
  const org = await prisma.organization.findUnique({
    where: { inviteCode: fullCode },
  })
  return org
}

▶ Exemplo: Páginas de Login e Registro

Saída:

TEXT 📖 Somente leitura
Atualiza o registro do banco de dados.
TSX
// src/app/(auth)/auth/signin/page.tsx
import { AuthCard } from "@/components/auth/auth-card"
import { SignInForm } from "@/components/auth/sign-in-form"

export default function SignInPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <AuthCard
        title="Entrar no TaskFlow"
        description="Insira suas credenciais para continuar"
      >
        <SignInForm />
      </AuthCard>
    </div>
  )
}

Saída:

TEXT 📖 Somente leitura
Renderiza a UI do componente SignInPage.
TSX
// src/components/auth/sign-in-form.tsx
"use client"

import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent } from "@/components/ui/card"

export function SignInForm() {
  const router = useRouter()
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setIsLoading(true)
    setError(null)

    const formData = new FormData(event.currentTarget)

    const result = await signIn("credentials", {
      email: formData.get("email") as string,
      password: formData.get("password") as string,
      redirect: false,
    })

    if (result?.error) {
      setError("Email ou senha inválidos")
      setIsLoading(false)
      return
    }

    router.push("/dashboard")
    router.refresh()
  }

  return (
    <Card>
      <CardContent className="pt-6">
        <form onSubmit={onSubmit} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              name="email"
              type="email"
              placeholder="alice@exemplo.com"
              required
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="password">Senha</Label>
            <Input
              id="password"
              name="password"
              type="password"
              placeholder="Digite sua senha"
              required
            />
          </div>
          {error && (
            <p className="text-sm text-red-500">{error}</p>
          )}
          <Button type="submit" className="w-full" disabled={isLoading}>
            {isLoading ? "Entrando..." : "Entrar"}
          </Button>
          <div className="relative">
            <div className="absolute inset-0 flex items-center">
              <span className="w-full border-t" />
            </div>
            <div className="relative flex justify-center text-xs uppercase">
              <span className="bg-background px-2 text-muted-foreground">
                Ou continue com
              </span>
            </div>
          </div>
          <Button
            type="button"
            variant="outline"
            className="w-full"
            onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
          >
            <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
              <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
              <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
              <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
              <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
            </svg>
            Entrar com Google
          </Button>
        </form>
      </CardContent>
    </Card>
  )
}


6. Proteção de Rotas com Middleware

(1) Fluxo de Trabalho do Middleware

100%
graph LR
    A[Requisição] --> B{Middleware}
    B -->|Caminho público| C[Permitir]
    B -->|Caminho protegido| D{Tem sessão?}
    D -->|Sim| C
    D -->|Não| E[Redirecionar /auth/signin]
    B -->|Rota de API| F{Token válido?}
    F -->|Sim| G[Encaminhar]
    F -->|Não| H[401 Unauthorized]

    style B fill:#ffeeba
    style C fill:#d4edda
    style E fill:#f8d7da

(2) Implementação do Middleware

TYPESCRIPT
// src/middleware.ts
import { auth } from "@/lib/auth"
import { NextResponse } from "next/server"

export default auth((req) => {
  const { nextUrl } = req
  const isLoggedIn = !!req.auth
  const isApiRoute = nextUrl.pathname.startsWith("/api")
  const isAuthRoute = nextUrl.pathname.startsWith("/auth")
  const isDashboardRoute = nextUrl.pathname.startsWith("/dashboard")
  const isPublicRoute =
    nextUrl.pathname === "/" ||
    nextUrl.pathname.startsWith("/_next") ||
    nextUrl.pathname.startsWith("/static") ||
    nextUrl.pathname === "/api/auth/signup"

  if (isApiRoute && !isPublicRoute && !isLoggedIn) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }

  if (isDashboardRoute && !isLoggedIn) {
    return NextResponse.redirect(new URL("/auth/signin", nextUrl))
  }

  if (isAuthRoute && isLoggedIn) {
    return NextResponse.redirect(new URL("/dashboard", nextUrl))
  }

  return NextResponse.next()
})

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico).*)",
  ],
}

▶ Exemplo: Testando a Proteção de Rotas do Middleware

BASH
# Acessando Rotas Protegidas (Não logado)
curl -v http://localhost:3000/dashboard 2>&1 | grep Location

# Visitando Rota de API (Não logado)
curl -v http://localhost:3000/api/projects 2>&1
TEXT 📖 Somente leitura
< Location: http://localhost:3000/auth/signin
< HTTP/1.1 302 Found

{"error":"Unauthorized"}

(3) Estratégias de Configuração do Matcher

Padrão Caminhos Correspondentes Caminhos Excluídos
/((?!_next/static).*) Todas as Rotas Recursos Estáticos
/((?!api/auth).*) Todas as Rotas API de Autenticação
`/((?!_next/static _next/image).*)` Todas as Rotas


7. Seeding do Banco de Dados

(1) Estratégia de Seeding

100%
graph TB
    A[seed.ts] --> B[Criar Organization]
    B --> C[Criar Users x5]
    C --> D[Criar Projects x5]
    D --> E[Criar Tasks x50]
    E --> F[Criar Comments x40]
    F --> G[Criar Attachments x10]

    style A fill:#cce5ff
    style G fill:#d4edda

▶ Exemplo: Script de Seeding Completo

Saída:

TEXT 📖 Somente leitura
Diagrama do fluxo de seeding: criar organização → usuários → projetos → tarefas → comentários.
TYPESCRIPT
// src/prisma/seed.ts
import { PrismaClient, Role, TaskStatus, TaskPriority } from "@prisma/client"
import { hash } from "bcryptjs"

const prisma = new PrismaClient()

async function main() {
  console.log("🌱 Semeando banco de dados...")

  const passwordHash = await hash("password123", 12)

  const org = await prisma.organization.create({
    data: {
      name: "Acme Corp",
      slug: "acme-corp",
      inviteCode: "TKFLW-ACMECORP",
    },
  })

  const users = await Promise.all([
    prisma.user.create({
      data: {
        name: "Alice Wang",
        email: "alice@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "OWNER",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=alice",
      },
    }),
    prisma.user.create({
      data: {
        name: "Bob Chen",
        email: "bob@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "ADMIN",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=bob",
      },
    }),
    prisma.user.create({
      data: {
        name: "Charlie Liu",
        email: "charlie@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "MEMBER",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=charlie",
      },
    }),
    prisma.user.create({
      data: {
        name: "Diana Park",
        email: "diana@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "MEMBER",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=diana",
      },
    }),
    prisma.user.create({
      data: {
        name: "Eve Zhang",
        email: "eve@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "VIEWER",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=eve",
      },
    }),
  ])

  const projectData = [
    { name: "Redesign do Site", description: "Redesenhar o site da empresa com UI moderna", color: "#3b82f6" },
    { name: "App Mobile v2", description: "Versão 2 do aplicativo mobile", color: "#10b981" },
    { name: "Integração de API", description: "Integrações de API de terceiros", color: "#f59e0b" },
    { name: "Migração de Dados", description: "Migrar dados do sistema legado", color: "#ef4444" },
    { name: "Auditoria de Segurança", description: "Auditoria de segurança e conformidade do Q3", color: "#8b5cf6" },
  ]

  const projects = await Promise.all(
    projectData.map((p) =>
      prisma.project.create({
        data: {
          ...p,
          organizationId: org.id,
          creatorId: users[0].id,
        },
      })
    )
  )

  const taskTitles = [
    "Configurar pipeline CI/CD", "Componentes do design system", "Escrever testes unitários",
    "Implementar autenticação de usuários", "Criar schema do banco de dados", "Documentação da API",
    "Otimização de desempenho", "Auditoria de acessibilidade", "Layout responsivo mobile",
    "Middleware de tratamento de erros", "Validação de dados", "Funcionalidade de busca",
    "Sistema de notificações", "Recurso de upload de arquivos", "Dashboard do usuário",
    "Painel de administração", "Módulo de relatórios", "Templates de email",
    "Integração WebSocket", "Camada de cache", "Rate limiting",
    "Sistema de logging", "Estratégia de backup", "Configuração de monitoramento",
    "Teste de carga",
  ]

  const statuses: TaskStatus[] = ["TODO", "IN_PROGRESS", "DONE"]
  const priorities: TaskPriority[] = ["LOW", "MEDIUM", "HIGH", "URGENT"]

  for (const project of projects.slice(0, 3)) {
    const taskCount = project.name === "Redesign do Site" ? 20 : 15

    for (let i = 0; i < taskCount; i++) {
      const task = await prisma.task.create({
        data: {
          title: taskTitles[i % taskTitles.length],
          description: `Descrição detalhada da tarefa #${i + 1} em ${project.name}`,
          status: statuses[i % statuses.length],
          priority: priorities[i % priorities.length],
          projectId: project.id,
          assigneeId: users[i % users.length].id,
          creatorId: users[0].id,
          order: i,
          dueDate: new Date(Date.now() + (i + 1) * 86400000 * 7),
        },
      })

      if (i % 3 === 0) {
        await prisma.comment.create({
          data: {
            content: `Esta tarefa está progredindo bem. É necessário revisar a implementação.`,
            taskId: task.id,
            authorId: users[(i + 1) % users.length].id,
          },
        })
      }
    }
  }

  const totalTasks = await prisma.task.count()
  const totalComments = await prisma.comment.count()

  console.log(`✅ Seeding concluído:
  - 1 Organização (${org.name})
  - ${users.length} Usuários
  - ${projects.length} Projetos
  - ${totalTasks} Tarefas
  - ${totalComments} Comentários`)
}

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

Saída:

TEXT 📖 Somente leitura
🌱 Semeando banco de dados...
✅ Seeding concluído:
  - 1 Organização (Acme Corp)
  - 5 Usuários
  - 5 Projetos
  - 50 Tarefas
  - 16 Comentários
BASH
# package.json Adicionar script de seed
# "prisma": { "seed": "tsx src/prisma/seed.ts" }

# Executar seeding
npx prisma db seed
TEXT 📖 Somente leitura
🌱 Seeding database...
✅ Seeding complete:
  - 1 Organization (Acme Corp)
  - 5 Users
  - 5 Projects
  - 50 Tasks
  - 16 Comments


8. Exemplo Completo: Todo o Processo da Inicialização do Projeto à Autenticação

BASH
# ============================================
# Processo Completo de Inicialização do TaskFlow
# ============================================

# 1. Criar um Projeto
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd taskflow

# 2. Instalar Dependências
npm install next-auth@beta @prisma/client @auth/prisma-adapter bcryptjs
npm install prisma tsx --save-dev
npm install @types/bcryptjs --save-dev

# 3. Inicializar shadcn/ui
npx shadcn@latest init -d
npx shadcn@latest add button card input label select dialog dropdown-menu avatar badge separator

# 4. Inicializar Prisma
npx prisma init --datasource-provider postgresql
# Copiar conteúdo do schema.prisma
npx prisma migrate dev --name init
npx prisma generate

# 5. Configurar Variáveis de Ambiente (.env)
cat > .env << EOF
DATABASE_URL="postgresql://postgres:password@localhost:5432/taskflow"
AUTH_SECRET="my-super-secret-key-change-in-production"
AUTH_URL="http://localhost:3000"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
EOF

# 6. Executar Seeding
npx prisma db seed

# 7. Iniciar o servidor de desenvolvimento
npm run dev

Saída esperada (ao acessar http://localhost:3000 no navegador):

TEXT 📖 Somente leitura
→ Redirecionar automaticamente para /auth/signin (Não logado)
→ Exibir o formulário de login (Email + Senha + Google)
→ Usar alice@acme.com / password123 para Fazer Login
→ Ir para /dashboard
→ Barra Lateral Exibe: Acme Corp · 5 Projetos · 50 Tarefas

❓ Perguntas Frequentes

P: Por que usar Auth.js em vez de Clerk? R: Auth.js (NextAuth v5) é uma solução open-source que suporta mais de 80 provedores e evita vendor lock-in. Clerk é adequado para prototipagem rápida, mas se torna bastante caro quando você começa a pagar. Com Auth.js, o TaskFlow pode controlar totalmente a lógica de autenticação e se integra perfeitamente com o Prisma.

P: Como sincronizar o banco de dados após modificar o schema do Prisma? R: Execute npx prisma migrate dev --name <descrição> para criar uma nova migração; o Prisma gerará e executará automaticamente o SQL. Em um ambiente de desenvolvimento, você pode usar npx prisma db push para sincronizar diretamente (sem gerar arquivos de migração).

P: Qual é o propósito do prefixo no código de convite TKFLW-? R: O prefixo identifica o código como um código de convite do TaskFlow para evitar confusão com outros sistemas. Exemplo de código de convite completo: TKFLW-ACMECORP. A combinação do prefixo com o slug da organização torna o código de convite altamente legível e único.

P: Por que a configuração do matcher no middleware corresponde a todos os caminhos? R: A configuração do matcher /((?!_next/static|_next/image|favicon.ico).*) corresponde a todos os caminhos de recursos não estáticos, permitindo que o middleware realize verificações de autenticação em todas as rotas de página e rotas de API. O Next.js recomenda realizar verificações de caminho dentro do middleware em vez de segmentá-las excessivamente no matcher.

P: O que é tsx no Seeding? R: tsx é o executor TypeScript (npm i tsx -D), que pode executar arquivos .ts diretamente sem compilação. O Prisma recomenda oficialmente o tsx como executor de seed. Você também pode usar ts-node, mas o tsx é mais rápido e oferece melhor compatibilidade.

P: Por que passwordHash é opcional no modelo User? R: Porque o login com Google OAuth é suportado e usuários OAuth não exigem senha. Para usuários que fazem login com Credentials, passwordHash tem um valor; para usuários OAuth, passwordHash é null. Isso demonstra a flexibilidade do design do schema.


📖 Resumo

📝 Exercícios

  1. Exercício Básico (⭐): Siga as etapas desta lição para inicializar o projeto TaskFlow, execute npm run dev com sucesso e visualize a página de login no navegador. Tire uma captura de tela dos resultados e salve-a.

  2. Exercício Avançado (⭐⭐): Adicione um modelo Label (sistema de tags) ao Prisma Schema, estabeleça um relacionamento muitos-para-muitos com Task (através da tabela intermediária TaskLabel), execute a migração e gere os tipos. Implemente uma rota de API CRUD para tags.

  3. Desafio (⭐⭐⭐): Estenda a configuração do Auth.js para adicionar um provedor GitHub OAuth; implemente a lógica de verificação de papéis descrita no middleware.ts — apenas usuários com os papéis OWNER e ADMIN podem acessar o caminho /dashboard/admin; escreva testes unitários para verificar a lógica de proteção de papéis.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%