Imported from zoeyzou/invoice-dashboard (
starter-code/AGENTS.md). Install upstream withnpx skills add zoeyzou/invoice-dashboard --skill starter-code. Copyright stays with the author.
Agent Guidelines for Invoice App
This document provides guidelines for AI agents working on this codebase.
Project Overview
This is a fullstack invoice management application built with:
- T3 Stack: Next.js 15+ (App Router), tRPC, Prisma
- Architecture: Domain-Driven Design (DDD) + Feature-first / Screaming Architecture
- Methodology: Test-Driven Development (TDD)
- UI: shadcn/ui + Tailwind CSS
Architecture Overview
The codebase uses a domain-driven, feature-first structure. Code is organized by business domains, not technical layers.
Folder Structure
src/
├── app/ # Next.js App Router (routes only)
├── domains/
│ └── invoice/ # Invoice domain (self-contained)
│ ├── api/ # tRPC router + schemas
│ ├── domain/ # DDD: entities, value-objects, services, repositories
│ ├── infrastructure/ # Prisma repository implementation
│ ├── ui/ # React components, hooks, types (view model)
│ └── *.e2e.spec.ts # E2E tests (colocated)
└── shared/ # Cross-cutting concerns
├── api/ # tRPC setup, root router, trpc-client
├── db/ # Prisma client
├── domain/ # Result type, errors
├── lib/ # utils (cn, etc.)
├── styles/ # globals.css
├── test/ # Vitest setup
└── ui/ # Shared components (SideNav, ThemeScript, ui primitives)
Dependency Rules (Critical)
- app imports from
domains/*andshared/*only - domains import from
shared/*only — no cross-domain imports - shared has no dependencies on domains
Key Principles
1. Dependency Direction
Infrastructure <- Domain -> Presentation
- Domain layer has NO dependencies on infrastructure or presentation
- Infrastructure implements domain interfaces
- Presentation consumes domain entities
2. Result Type Pattern
Use Result<T, E> for operations that can fail:
import { Result, ok, err } from "~/shared/domain/result";
function createInvoice(data: CreateInvoiceData): Result<Invoice, DomainError> {
if (!isValid(data)) {
return err(new ValidationError("Invalid invoice data"));
}
return ok(new Invoice(...));
}
3. No Business Logic in Components/Routers
// ❌ Bad: Business logic in component
function InvoiceForm() {
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// ...
}
// ✅ Good: Business logic in domain service
function InvoiceForm() {
const invoice = new Invoice(...);
const total = invoice.calculateTotal(); // Domain method
}
4. Repository Pattern
// Domain interface (domains/invoice/domain/repositories/)
export interface InvoiceRepository {
findById(id: InvoiceId): Promise<Result<Invoice, NotFoundError>>;
}
// Infrastructure implementation (domains/invoice/infrastructure/)
export class PrismaInvoiceRepository implements InvoiceRepository {
async findById(id: InvoiceId): Promise<Result<Invoice, NotFoundError>> {
// Prisma implementation
}
}
File Organization
Creating a New Domain
Add a new folder under domains/{domain-name}/ with: api/, domain/, infrastructure/, ui/.
Creating a New Feature (e.g., within invoice)
-
Domain Layer First (TDD)
// domains/invoice/domain/entities/invoice.entity.ts export class Invoice { // Business logic here } -
Value Objects
// domains/invoice/domain/value-objects/invoice-id.vo.ts export class InvoiceId { // Type-safe ID } -
Repository Interface
// domains/invoice/domain/repositories/invoice.repository.interface.ts export interface InvoiceRepository { // Methods } -
Prisma Implementation
// domains/invoice/infrastructure/invoice.prisma.repository.ts export class PrismaInvoiceRepository implements InvoiceRepository { // Prisma implementation (import db from ~/shared/db) } -
tRPC Router
// domains/invoice/api/invoice.router.ts export const invoiceRouter = createTRPCRouter({ create: publicProcedure .input(createInvoiceSchema) .mutation(async ({ input, ctx }) => { // Delegate to domain service }), });Register in
shared/api/root.ts. -
UI Layer
// domains/invoice/ui/types.ts — view model / DTO export interface Invoice { ... } // domains/invoice/ui/components/InvoiceList.tsx export function InvoiceList() { // React component using tRPC hooks (~/shared/api/trpc-client/react) }
Testing Strategy
TDD Workflow
- Red: Write failing test
- Green: Implement minimum code to pass
- Refactor: Improve while keeping tests green
Test Structure (Colocated)
- Unit tests: Colocated with source (
*.test.ts,*.test.tsx) - E2E tests: Colocated in domain (
domains/invoice/*.e2e.spec.ts) - Vitest setup:
shared/test/setup.ts
Example Test
// domains/invoice/domain/entities/invoice.entity.test.ts
import { describe, it, expect } from "vitest";
import { Invoice } from "~/domains/invoice/domain/entities/invoice.entity";
describe("Invoice", () => {
it("should calculate total correctly", () => {
const invoice = new Invoice(/* ... */);
expect(invoice.total).toBe(35);
});
});
Common Tasks
Adding a New Domain Entity
- Create entity class in
domains/{domain}/domain/entities/ - Add value objects if needed in
domains/{domain}/domain/value-objects/ - Write tests first (TDD), colocated
- Implement business logic
- Create repository interface in
domains/{domain}/domain/repositories/ - Implement Prisma repository in
domains/{domain}/infrastructure/
Adding a New tRPC Endpoint
- Define Zod schema in
domains/{domain}/api/ - Create procedure in
domains/{domain}/api/{domain}.router.ts - Delegate to domain service
- Use Result type for error handling
- Write integration test
Adding a New UI Component
- Create component in
domains/{domain}/ui/components/ - Use tRPC hooks from
~/shared/api/trpc-client/react - Consume domain types from
domains/{domain}/ui/types.ts - Write component tests, colocated
Commit Message Guidelines
Critical Rule
Every commit message must clearly indicate what the commit does. The commit history should be readable and self-documenting. Someone reading git log should understand what happened without reading the code.
Format
type(scope): clear description
Optional detailed body explaining context, why, and how.
Commit Types
feat: New featurefix: Bug fixrefactor: Code refactoring (no behavior change)test: Adding or updating testsdocs: Documentation changeschore: Maintenance tasks (deps, config, etc.)style: Code style changes (formatting, etc.)perf: Performance improvements
Scope Examples
invoice- Invoice domain/featuredomain- Domain layer changesapi- API/tRPC changesui- UI component changesinfra- Infrastructure changesconfig- Configuration changes
Description Best Practices
✅ DO:
- Write clear, descriptive messages:
feat(invoice): add create invoice use case with validation - Use imperative mood: "add feature" not "added feature"
- Keep first line under 72 characters
- Add body for complex changes explaining context
- Explain WHAT and WHY, not just WHAT
❌ DON'T:
- Use vague messages: "update", "fix stuff", "changes"
- Reference issues/PRs without context
- Write messages requiring diff reading to understand
- Use past tense: "added feature" (use "add feature")
Examples
Good:
feat(invoice): add create invoice use case with validation
Implements the create invoice functionality with:
- Domain validation for all required fields
- Invoice ID generation service
- tRPC endpoint with Zod schema validation
- Error handling using Result type pattern
fix(domain): correct invoice ID generation to ensure uniqueness
The previous implementation could generate duplicate IDs.
Now uses repository to check existence before returning ID.
Adds retry logic with max attempts to prevent infinite loops.
Bad:
fix: stuff
update invoice
WIP
fix bug
changes
feat: invoice
refactor: code
Commit History Quality Checklist
- Commit message clearly describes what changed
- Commit is atomic (one logical change)
- Related changes are grouped together
- Commit history tells a coherent story
- No "WIP" or temporary commit messages
- Complex changes have explanatory body
Quality Gates
Before submitting code:
- All tests pass
- TypeScript compiles without errors
- No
anytypes (unless justified) - Domain layer has no framework dependencies
- Business logic is in domain layer
- Error handling uses Result type
- Dependency rules respected: app → domains/shared; domains → shared only; shared → nothing
- No cross-domain imports
- Commit message clearly indicates what it does
- Commit message follows conventional commits format
