The boilerplate implements a strict 3-layer architecture that ensures separation of concerns, maintainability, and scalability. Each layer has specific responsibilities and communicates through well-defined interfaces.
Perfect for: Building maintainable, testable, and scalable SaaS applications with clear separation between UI, business logic, and data access.
The system follows a unidirectional data flow through three main layers:
3-Layer Architecture:
๐ฅ๏ธ PRESENTATION LAYER
โโโ React Server Components (RSC)
โโโ Client Components ("use client")
โโโ Server Actions (mutations)
โโโ DAL (Data Access Layer with cache)
โฌ๏ธ
โ๏ธ SERVICE LAYER
โโโ Business Logic
โโโ Validation (Zod schemas)
โโโ Authorization (CASL RBAC)
โโโ Facades (interfaces)
โโโ Interceptors (logging, audit)
โฌ๏ธ
๐๏ธ PERSISTENCE LAYER
โโโ Drizzle ORM
โโโ Database Models
โโโ Repositories (DAO pattern)
โโโ Type Transformations
Strict 3-layer separation
Clear data movement
'use cache' + cacheTagRBAC with CASL
Optimized at every layer
Here's how a typical user operation flows through all layers:
Get User Profile:
// ๐ฅ๏ธ PRESENTATION (RSC Component)
import { getUserByIdDal } from '@/app/dal/user-dal'
export default async function UserProfile({ userId }: { userId: string }) {
const user = await getUserByIdDal(userId) // Cached read
return <div>{user.name}</div>
}
// ๐ DAL (Data Access Layer)
import { cache } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
import { getUserByIdService } from '@/services/facades/user-service-facade'
// 'use cache' makes the read cacheable across requests; cache() from React
// still deduplicates it within a single request.
export const getUserByIdDal = cache(async (id: string) => {
'use cache'
cacheLife('hours')
cacheTag(`user:${id}`)
const userModel = await getUserByIdService(id)
return transformToUserDTO(userModel) // Transform to domain type
})
// โ๏ธ SERVICE (Business Logic)
export const getUserByIdService = async (id: string) => {
const parsed = userUuidSchema.safeParse(id) // Validation
if (!parsed.success) throw new ValidationError()
const granted = await canReadUser(parsed.data) // Authorization
if (!granted) throw new AuthorizationError()
return await getUserByIdDao(parsed.data) // Call repository
}
// ๐๏ธ REPOSITORY (Data Access)
export async function getUserByIdDao(id: string): Promise<UserModel> {
return await db.query.users.findFirst({
where: eq(users.id, id)
})
}RSC, Client Components, Server Actions, DAL, and Forms
Business logic, validation, authorization, facades, and interceptors
Drizzle ORM, models, repositories, and type transformations
Client/server separation and utility organization
File organization, naming conventions, and import rules
Feature development process and testing strategies
Architecture ready! Your application follows enterprise-grade patterns that ensure maintainability, testability, and scalability as your SaaS grows.