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/authpara 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
leadto 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
Quer desenvolver um SaaS em um final de semana?
Um único comando no terminal e pronto, você já tem seu projeto criado, com site, blog, central de ajuda, autenticação, onboarding, dashboard, emails... Para resumir, é assim que eu crio os meus SaaS em um Final de Semana.
Conheça minha estratégia