Primeiros Passos com o SaaS Boilerplate
Agora que você já configurou seu ambiente de desenvolvimento, é hora de começar a construir sua aplicação SaaS. Este guia vai te orientar pelos primeiros passos essenciais para se familiarizar com o SaaS Boilerplate e criar seu primeiro recurso.
Entendendo a Arquitetura
Antes de mergulhar no código, é importante entender a arquitetura do SaaS Boilerplate. O projeto segue uma arquitetura baseada em recursos com clara separação de responsabilidades, impulsionada pelo Igniter.js para desenvolvimento de API com segurança de tipos.
Diretórios Principais
src/saas-boilerplate/
: Contém recursos principais do SaaS como autenticação, faturamento e organizaçõessrc/app/
: Páginas do Next.js app router organizadas por seção (dashboard, marketing, auth)src/content/
: Conteúdo Markdown e MDX para documentação, posts de blog, etc.src/core/
: Componentes principais da aplicação, provedores e utilitáriossrc/features/
: Aqui é onde ficarão os recursos específicos da sua aplicação
Explorando o Boilerplate
Começe explorando os recursos e páginas existentes para entender como estão implementados. Aqui estão algumas áreas importantes para verificar:
- Fluxo de Autenticação: Navegue até
/src/saas-boilerplate/features/auth
para ver como a autenticação está implementada - Páginas do Dashboard: Verifique o diretório
/src/app/(dashboard)
para ver a interface do dashboard - Páginas de Marketing: Explore
/src/app/(marketing)
para ver a página inicial e outros conteúdos de marketing - Rotas da API: Examine os controladores em vários módulos de recursos para entender a estrutura da API
Criando Seu Primeiro Recurso
Vamos ver como criar seu primeiro recurso usando o Igniter.js. Vamos criar um recurso simples de "Notas" como exemplo, seguindo nossa arquitetura multi-tenant onde as organizações são os principais inquilinos.
Passo 1: Definir o Esquema do Banco de Dados
Primeiro, defina o modelo de dados do seu recurso no esquema Prisma. Observe que relacionamos entidades a organizações, não a usuários:
// prisma/schema.prisma model Note { id String @id @default(cuid()) title String content String organization Organization @relation(fields: [organizationId], references: [id]) organizationId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
Execute a migração para atualizar seu banco de dados:
bun db:migrate:dev --name add_notes
Passo 2: Gerar o Recurso
Usando a CLI do Igniter.js, gere um novo esqueleto de recurso:
bun igniter generate feature
Selecione "Notes" das opções disponíveis com base no seu esquema Prisma. Isso criará:
- Tipos e interfaces
- Controladores que lidam com requisições HTTP
- Procedimentos que contêm lógica de negócios
- Rotas de API com permissões adequadas
Passo 3: Entendendo os Arquivos Gerados
Vamos olhar os componentes principais:
Tipos (notes.interfaces.ts)
export interface Note { id: string title: string content: string organizationId: string organization?: Organization createdAt: Date updatedAt: Date } export interface CreateNoteDTO { title: string content: string organizationId: string } export interface NoteQueryParams { page?: number limit?: number sortBy?: string sortOrder?: 'asc' | 'desc' search?: string organizationId: string }
Controlador com Permissões (notes.controller.ts)
export const NotesController = igniter.controller({ name: 'notes', path: '/notes', actions: { findMany: igniter.query({ method: 'GET', path: '/', use: [NotesFeatureProcedure(), AuthFeatureProcedure()], handler: async ({ response, request, context }) => { const auth = await context.auth.getSession({ requirements: 'authenticated', roles: ['admin', 'owner', 'member'] }) const result = await context.notes.findMany({ ...request.query, organizationId: auth.organization.id }) return response.success(result) } }) } })
Procedimento (notes.procedure.ts)
export const NotesFeatureProcedure = igniter.procedure({ name: 'NotesFeatureProcedure', handler: async (_, { context }) => { return { notes: { findMany: async (query: NoteQueryParams): Promise<Note[]> => { const result = await context.providers.database.note.findMany({ where: query.search ? { OR: [ { title: { contains: query.search } }, { content: { contains: query.search } } ], organizationId: query.organizationId } : { organizationId: query.organizationId }, skip: query.page ? (query.page - 1) * (query.limit || 10) : undefined, take: query.limit, orderBy: query.sortBy ? { [query.sortBy]: query.sortOrder || 'asc' } : undefined }) return result as Note[] } } } } })
Passo 4: Entendendo a Arquitetura
- Design Multi-tenant: Todos os recursos são limitados a organizações, não a usuários individuais
- Sistema de Permissões:
- Use
AuthFeatureProcedure()
para lidar com autenticação - Defina papéis necessários (
admin
,owner
,member
) - Acesse o contexto da organização via
auth.organization.id
- Use
- Separação de Responsabilidades:
- Controladores: Lidam com requisições HTTP e permissões
- Procedimentos: Contêm lógica de negócios e interagem com provedores
- Provedores: Lidam com operações de banco de dados e serviços externos
Passo 5: Registrar o Recurso
Registre seu recurso no roteador:
// src/features/notes/notes.router.ts import { NotesController } from './controllers/notes.controller' export const notesRouter = { controllers: [NotesController] }
Next Steps
- Explore existing features like
lead
to understand best practices - Implement client-side components using the generated API
- Add custom business logic in procedures
- Extend the permission system for your specific needs
Using Your Feature
Let's look at some examples of how to use your feature in different contexts:
Client-Side Form Example
// src/features/notes/presentation/components/note-upsert-sheet.tsx 'use client' import { z } from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { api } from '@/igniter.client' const schema = z.object({ title: z.string().min(1, 'Title is required'), content: z.string().min(1, 'Content is required') }) export function NoteUpsertSheet() { const form = useForm({ resolver: zodResolver(schema) }) const { mutate: createNote } = api.notes.create.useMutation({ onSuccess: () => { form.reset() } }) const onSubmit = form.handleSubmit((data) => { createNote(data) }) return ( <form onSubmit={onSubmit}> <input {...form.register('title')} /> <textarea {...form.register('content')} /> <button type="submit">Create Note</button> </form> ) }
Client-Side Query Example
// src/app/(dashboard)/app/organizations/page.tsx 'use client' import { api } from '@/igniter.client' export default function OrganizationSwitcher() { const { data: organizations } = api.organization.findMany.useQuery() return ( <div> {organizations?.map((org) => ( <div key={org.id}> {org.name} </div> ))} </div> ) }
Server-Side Query Example
// src/app/(dashboard)/app/leads/page.tsx import { api } from '@/igniter.server' export default async function LeadsPage() { const contacts = await api.lead.findMany.query() return ( <LeadDataTableProvider initialData={contacts.data ?? []}> <PageWrapper> <PageHeader> <PageTitle>Leads</PageTitle> </PageHeader> </PageWrapper> </LeadDataTableProvider> ) }
Check out other guides to learn more about:
- Role-based access control
- Organization management
- Custom providers and services
Acelere seu desenvolvimento
Construa aplicações SaaS completas em minutos com nosso boilerplate moderno. Autenticação, pagamentos, gerenciamento de usuários e muito mais!
Conheça o SaaS Boilerplate