Skip to content
SaaS4Builders
Frontend

Vertical Slices

The features/ directory structure, layer hierarchy, barrel exports, and how each feature module is organized in the Nuxt 4 frontend.

The frontend uses a vertical slice architecture. Each feature is a self-contained module with its own types, schemas, API client, composables, components, and stores. Features are organized into three layers with strict dependency rules.


Layer Hierarchy

frontend/
├── features/
│   ├── foundation/    # Layer 0 — Auth, tenancy, entitlements, settings, profile
│   ├── core/          # Layer 1 — Business domains: billing, catalog, team, notifications
│   └── product/       # Layer 2 — Product features: onboarding, analytics, platform
└── common/            # Cross-cutting utilities (auto-imported)

Dependency Rules

Dependencies flow downward only:

product/ → can import → core/, foundation/, common/
core/    → can import → foundation/, common/
foundation/ → can import → common/ only
common/  → imports NOTHING from features/

These rules are enforced by an ESLint rule. Violating the hierarchy — for example, importing from core/team inside foundation/auth — is rejected at lint time.

What Goes Where

LayerContentsExamples
foundation/Infrastructure that every feature depends onAuth store, tenant resolution, entitlements, user profile, settings, currencies
core/Business domains with API endpointsBilling, team management, product catalog, notifications
product/End-user product featuresOnboarding flow, analytics dashboard, platform admin
common/Shared utilities with no feature dependenciesAPI client, case transforms, money formatting, UI components

Feature Module Structure

Every feature in core/ or product/ follows the same file structure:

features/core/<feature>/
├── index.ts           # Barrel exports — public API (REQUIRED)
├── types.ts           # TypeScript types (REQUIRED)
├── schemas.ts         # Zod validation schemas (REQUIRED)
├── api/               # API client composable (core/product only)
│   ├── index.ts       # Re-exports the API composable
│   └── <feature>.api.ts
├── composables/       # Reactive logic (useSubscription, useTeamMembers, etc.)
├── components/        # Vue components
├── stores/            # Pinia stores (if needed)
└── __tests__/         # Vitest tests
Foundation features have no api/ folder. They access the API directly through stores (e.g., the auth store calls /auth/me on bootstrap).

Real Example: The Billing Feature

Here is the actual structure of the billing feature — the most complex module in the codebase:

features/core/docs/billing/
├── index.ts                    # Barrel: 70+ exports
├── types.ts                    # 60+ types inferred from Zod
├── schemas.ts                  # All Zod schemas + status constants
├── api/
│   ├── index.ts                # Re-exports useBillingApi
│   ├── billing.api.ts          # 15+ API methods
│   └── public-catalog.api.ts   # Public (unauthenticated) catalog API
├── composables/
│   ├── useSubscription.ts      # Current subscription + cancel/resume
│   ├── useInvoices.ts          # Invoice list + PDF download
│   ├── usePlans.ts             # Available plans + eligibility
│   ├── useCheckout.ts          # Checkout flow orchestration
│   ├── useSeatBilling.ts       # Seat-based billing info
│   ├── useBillingPortal.ts     # Stripe billing portal
│   ├── useUsage.ts             # Usage metering
│   └── usePublicCatalog.ts     # Public pricing page
├── components/
│   ├── BillingSubscriptionCard.vue
│   ├── BillingSubscriptionStatus.vue
│   ├── BillingInvoiceList.vue
│   ├── BillingCancelModal.vue
│   ├── BillingChangePlanModal.vue
│   ├── BillingSeatInfo.vue
│   ├── UsageMeterCard.vue
│   └── ... (15+ components total)
├── schemas/
│   └── public-catalog.schemas.ts
├── types/
│   └── public-catalog.types.ts
└── __tests__/

Barrel Exports

Every feature has an index.ts that defines its public API. Other modules import only through this barrel — never from internal files.

frontend/features/core/docs/billing/index.ts
// ─── Types ─────────────────────────────────────────────────────────
export type {
  Subscription, Invoice, BillingPlan, CheckoutSession,
  PlanFeature, SeatBilling, ProrationPreview,
  // ... 30+ more types
} from './types'

// ─── Status Constants ──────────────────────────────────────────────
export {
  ACTIVE_STATUSES, CANCELLABLE_STATUSES, RESUMABLE_STATUSES,
  SUBSCRIPTION_STATUS_COLORS, INVOICE_STATUS_COLORS,
  // ... more constants
} from './schemas'

// ─── Schemas ───────────────────────────────────────────────────────
export {
  subscriptionSchema, invoiceSchema, billingPlanSchema,
  checkoutInputSchema, cancelSubscriptionInputSchema,
  // ... more schemas
} from './schemas'

// ─── API ───────────────────────────────────────────────────────────
export { useBillingApi } from './api'

// ─── Composables ───────────────────────────────────────────────────
export { useSubscription } from './composables/useSubscription'
export { useInvoices } from './composables/useInvoices'
export { usePlans } from './composables/usePlans'
export { useCheckout } from './composables/useCheckout'
export { useSeatBilling } from './composables/useSeatBilling'

// ─── Components ────────────────────────────────────────────────────
export { default as BillingSubscriptionCard } from './components/BillingSubscriptionCard.vue'
export { default as BillingInvoiceList } from './components/BillingInvoiceList.vue'
// ... more components

Import Rules

// ✅ Import through the barrel
import { useSubscription, type Subscription } from '@core/docs/billing'

// ❌ NEVER import internals
import { helper } from '@core/docs/billing/_internal/...'

Foundation features are auto-imported by Nuxt — you use them directly:

// Foundation: no import needed
const { tenant, hasTenantPermission } = useCurrentTenant()
const authStore = useAuthStore()

The common/ Directory

common/ contains cross-cutting utilities that have no feature dependencies. It is auto-imported by Nuxt.

common/
├── api/
│   ├── client.ts               # useApiClient — centralized HTTP client
│   ├── errors.ts               # Typed error classes (ApiError, ValidationError, ...)
│   ├── schema.ts               # Pagination + error response Zod schemas
│   ├── url.ts                  # URL builder utility
│   └── _lib/
│       └── billing-helpers.ts  # PDF download, billing error helpers
├── composables/
│   ├── useAuthenticatedAsyncData.ts  # SSR-safe data fetching wrapper
│   ├── useLocalizedCollection.ts
│   ├── useRoleLabel.ts
│   └── useZodI18nValidation.ts
├── components/
│   ├── layout/                 # AppLogo, Header, Footer, LanguageSelector
│   ├── marketing/              # HeroBackground, PromotionalVideo
│   └── ui/                     # ConfirmModal, ImagePlaceholder
├── types/
│   ├── content.ts              # Nuxt Content collection types
│   └── money.ts                # Money schema + type
└── utils/
    ├── index.ts                # Barrel: toCamelCase, toSnakeCase, formatMoney, ...
    └── _lib/
        ├── caseTransform.ts    # Deep snake_case ↔ camelCase conversion
        ├── cookie.ts           # XSRF token reader
        ├── money.ts            # Locale-aware money formatting
        ├── pricing.ts          # Static pricing helpers
        ├── queryBuilder.ts     # Spatie QueryBuilder URL params
        └── currency-decimals.ts # ISO 4217 minor units

Key utilities:

  • useApiClient() — Centralized HTTP client with CSRF handling, tenant headers, error interceptors, and automatic case transformation
  • useAuthenticatedAsyncData() — Wraps useAsyncData with server: false for SSR-safe authenticated data fetching
  • toCamelCase() / toSnakeCase() — Deep recursive key transformation bridging Laravel's snake_case API and TypeScript's camelCase
  • formatMoney() — Locale-aware money formatting with ISO 4217 decimal handling

Cross-Feature Imports

Within core/, features are isolated from each other. The exception is catalog, which is designated as an "owner" feature that other core features can import:

// ✅ billing imports from catalog (catalog is an owner feature)
import { usePlans } from '@core/catalog'

// ❌ billing imports from team (not allowed)
import { useTeamMembers } from '@core/team'

If two core features need to share data, the coordination happens at the page level — the page imports both composables and passes data between them.


What's Next