Perfect for: Understanding the codebase organization, following established patterns, and maintaining consistency as your application grows.
Project Overview
ShipSaaS is a monolithic Next.js SaaS boilerplate with a well-organized layered architecture. It emphasizes clear separation of concerns with dedicated layers for routing, components, services, and database access.Project Root Organization:Key Principles:
shipsaas-ai-boilerplate/
├── src/ # 📁 Application source code
├── public/ # 📁 Static assets (images, fonts)
├── docs/ # 📚 Documentation
├── messages/ # 🌐 i18n translation files
├── scripts/ # 🔧 Build/database scripts
├── drizzle/ # 🗄️ Database migrations
├── e2e/ # 🧪 End-to-end tests
├── .github/ # 📋 GitHub workflows
├── .claude/ # 🤖 Claude Code configuration
├── .cursor/ # 📋 Cursor IDE rules
├── package.json # Dependencies configuration
├── next.config.ts # Next.js configuration
├── tsconfig.json # TypeScript configuration
├── drizzle.config.ts # Drizzle ORM configuration
└── playwright.config.ts # E2E testing configuration
- Single application - All code in one cohesive Next.js project
- Layered architecture - Clear separation: routes → components → services → database
- Feature-based organization - Components grouped by feature
- Service layer pattern - Business logic isolated in services
- Type safety - Full TypeScript coverage with Drizzle ORM
Naming Conventions
Consistent naming patterns across the codebase improve readability and maintainability:File Naming Patterns (ShipSaaS Standard):Directory Naming:File Organization Rules:
✅ GOOD FILE NAMES:
# Components (PascalCase - React convention)
UserProfile.tsx
SubscriptionCard.tsx
PaymentForm.tsx
UserAvatar.tsx
NotificationBell.tsx
# Pages & Layouts (Next.js conventions)
page.tsx
layout.tsx
loading.tsx
error.tsx
not-found.tsx
# Services (kebab-case with suffix)
user-service.ts
subscription-service.ts
email-service.ts
organization-service.ts
# Service Facades (kebab-case with suffix)
user-service-facade.ts
subscription-service-facade.ts
email-service-facade.ts
# Database Models (kebab-case)
user.ts # Schema definition
organization.ts
subscription.ts
# Repositories (kebab-case with suffix)
user-repository.ts
organization-repository.ts
subscription-repository.ts
# Utilities & Helpers (kebab-case)
user-helper.ts
date-helper.ts
api-client.ts
common-helper.ts
# Validation Schemas (kebab-case with suffix)
user-validation.ts
subscription-validation.ts
# Context & Hooks (camelCase for hooks, PascalCase for Context)
useUserData.ts
useSubscription.ts
UserContext.tsx
ThemeContext.tsx
✅ GOOD DIRECTORY NAMES:
# Feature directories (kebab-case)
/user/
/organization/
/subscription/
/payment/
/api-key/
# Component feature directories (kebab-case)
/checkout-stripe/
/user-profile/
/admin-dashboard/
/project-management/
# Service subdirectories (kebab-case)
/authentication/
/authorization/
/validation/
/errors/
# Next.js special directories (exact names)
/(public)/
/(auth)/
/(app)/
/admin/
/api/
- PascalCase: React components (.tsx files)
- kebab-case: Services, utilities, hooks, repositories, models
- camelCase: React hooks (useMyHook.ts)
- lowercase: Next.js special files (page.tsx, layout.tsx, etc.)
- SCREAMING_SNAKE_CASE: Constants only
Import Organization
Consistent import patterns improve code readability and dependency tracking:Import Order and Grouping (ShipSaaS Standard):Path Aliases Configuration:
// ✅ GOOD: Organized import groups
// 1. Node.js built-ins
import fs from 'fs'
import path from 'path'
// 2. External libraries
import React from 'react'
import { z } from 'zod'
import { eq } from 'drizzle-orm'
// 3. Internal services and business logic
import { getUserByIdService } from '@/services/facades/user-service-facade'
import { getUserByIdRepository } from '@/db/repositories/user-repository'
// 4. UI components and utilities
import { Button, Card } from '@/components/ui'
import { UserProfile } from '@/components/features/user'
import { formatDate } from '@/lib/helper/date-helper'
// 5. Types and constants
import type { User } from '@/services/types/domain'
import { MAX_RETRY_ATTEMPTS } from '@/lib/constants'
// 6. Relative imports (local to current file)
import { localHelper } from './local-helper'
import './styles.css'
export function MyComponent() {
// Component implementation
}// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"], // Root src alias
"@/components/*": ["./src/components/*"], // Components
"@/services/*": ["./src/services/*"], // Services
"@/db/*": ["./src/db/*"], // Database
"@/lib/*": ["./src/lib/*"], // Utilities
"@/app/*": ["./src/app/*"], // App routes
"@/i18n/*": ["./src/i18n/*"] // i18n
}
}
}Code Organization Patterns
Consistent patterns for organizing code within files and directories:Service File Organization (ShipSaaS Pattern):Service Facade Organization:Component File Organization (ShipSaaS Pattern):Repository File Organization:
// src/services/user-service.ts
// 1. Imports (organized by groups)
import { eq } from 'drizzle-orm'
import type { User, CreateUserInput, UserUpdate } from './types/domain'
import { createUserValidationSchema } from './validation/user-validation'
import { canEditUser, canDeleteUser } from './authorization/user-authorization'
import { getUserByIdRepository, createUserRepository } from '@/db/repositories/user-repository'
// 2. Main service functions (public)
export const getUserByIdService = async (id: string): Promise<User | null> => {
const user = await getUserByIdRepository(id)
return user ?? null
}
export const createUserService = async (data: CreateUserInput): Promise<User> => {
const validated = createUserValidationSchema.parse(data)
return await createUserRepository(validated)
}
export const updateUserService = async (
id: string,
data: UserUpdate,
userId: string
): Promise<User> => {
// Authorization check
if (!canEditUser(userId, id)) {
throw new AuthorizationError('Cannot edit this user')
}
// Validation and update
return await updateUserRepository(id, data)
}
export const deleteUserService = async (id: string, userId: string): Promise<void> => {
if (!canDeleteUser(userId, id)) {
throw new AuthorizationError('Cannot delete this user')
}
await deleteUserRepository(id)
}// src/services/facades/user-service-facade.ts
// Public API - only exports from this file should be imported externally
export {
getUserByIdService,
createUserService,
updateUserService,
deleteUserService,
listUsersService
} from '../user-service'
export type { User, CreateUserInput, UserUpdate } from '../types/domain'// src/components/features/user/UserProfile.tsx
'use client'
// 1. Imports (organized)
import { useState } from 'react'
import { Button, Card } from '@/components/ui'
import { useUserData } from './hooks'
import type { User } from '@/services/types/domain'
// 2. Types and interfaces (local to component)
interface UserProfileProps {
userId: string
onEdit?: () => void
}
// 3. Component implementation
export default function UserProfile({ userId, onEdit }: UserProfileProps) {
const [isLoading, setIsLoading] = useState(false)
const { user, error } = useUserData(userId)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error loading user</div>
if (!user) return <div>User not found</div>
return (
<Card>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
{onEdit && <Button onClick={onEdit}>Edit Profile</Button>}
</Card>
)
}// src/db/repositories/user-repository.ts
import { eq } from 'drizzle-orm'
import db from '../client'
import { usersTable } from '../models/user'
import type { User, NewUser } from '../models/user'
// Query functions
export const getUserByIdRepository = async (id: string): Promise<User | null> => {
const result = await db.query.usersTable.findFirst({
where: eq(usersTable.id, id)
})
return result ?? null
}
export const createUserRepository = async (data: NewUser): Promise<User> => {
const [user] = await db.insert(usersTable).values(data).returning()
return user
}
export const updateUserRepository = async (
id: string,
data: Partial<User>
): Promise<User> => {
const [user] = await db
.update(usersTable)
.set(data)
.where(eq(usersTable.id, id))
.returning()
return user
}
export const deleteUserRepository = async (id: string): Promise<void> => {
await db.delete(usersTable).where(eq(usersTable.id, id))
}Best Practices
📁
File Organization
• Feature-based organization in components
• Barrel exports (index.ts) for clean paths
• Consistent kebab-case for files/directories
• Single responsibility per file
• Co-locate related code (components + hooks + types)
• Tests alongside source code (tests/)
⚙️
Service Layer Pattern
• Validate input with Zod schemas
• Check authorization before operations
• Use repositories for data access
• Expose via facades (public API only)
• Handle errors with custom classes
• Return typed results
📥
Import Management
• Grouped imports by type (libs, services, components)
• Use path aliases (@/, @/services, etc.)
• Import from facades (never direct services)
• Type-only imports for types (import type )
• Relative imports for local components
• Prevent cycles with layered imports
🏷️
Naming Consistency
• PascalCase: Components & Context (UserProfile.tsx)
• kebab-case: Services & utilities (user-service.ts)
• camelCase: Hooks & functions (useUserData, getData)
• SCREAMING_SNAKE_CASE: Constants only
• Suffixes: -service, -facade, -validation, -helper
• Prefixes: use (hooks), can/has/is (booleans)
🗄️
Database Layer
• Define schemas in models/ (Drizzle)
• Create repositories for data access
• Use in services (never in components)
• Export types from models for services
• Handle migrations via drizzle-kit
• Type-safe queries with Drizzle ORM
🧪
Testing Strategy
• Unit tests for services and utilities
• Component tests with Testing Library
• Integration tests for service workflows
• E2E tests with Playwright
• Mock external services (Stripe, email)
• Test both authorized & unauthorized access
🔒
Security & Authorization
• Check auth at service layer entry
• Validate permissions before operations
• Use authorization modules (RBAC)
• Protect API routes with middleware
• Validate input with Zod schemas
• Handle errors gracefully and securely
📦
Monolithic Structure
• Single Next.js app (not monorepo)
• Clear layer separation: Routes → Components → Services → DB
• Feature-based grouping in components
• Domain-based organization in services
• Shared lib/ for utilities
• Scalable for growth with clear patterns
ShipSaaS structure mastered! Your codebase follows clean, layered patterns that make development fast and maintainable. The monolithic structure with clear separation of concerns allows you to scale features efficiently while keeping the code organized and type-safe.