Styling Guides

File & Directory Structure

Organising code on disk so contributors can navigate without a map.

Overview

File and directory structure communicates the architecture of a codebase. Good structure makes it immediately clear where to find a given type of code, where to add new code, and what the relationship between modules is. The two primary organisation axes are feature-based (all code for a feature together) and layer-based (all controllers together, all models together).

Origin

Rails (DHH, 2004) popularised MVC-based layer structure (app/models, app/controllers, app/views) for web applications. The "screaming architecture" concept (Robert Martin, 2011) argued that directory names should reveal the domain, not the framework. Create React App established the src/ convention; Next.js 13 introduced the app/ directory with co-located server components.

Examples

Feature-based structure for a Next.js application

// Feature-based structure: each feature is self-contained
// src/
//   features/
//     orders/
//       components/
//         OrderCard.tsx
//         OrderList.tsx
//       hooks/
//         useOrders.ts
//         useOrderMutation.ts
//       services/
//         orderService.ts        // API calls
//       types/
//         order.types.ts
//       utils/
//         orderFormatters.ts
//       index.ts                 // public API of the feature
//     auth/
//       components/
//         LoginForm.tsx
//       hooks/
//         useAuth.ts
//       services/
//         authService.ts
//   shared/
//     components/
//       Button.tsx
//       Modal.tsx
//     hooks/
//       useDebounce.ts
//     utils/
//       formatDate.ts
//   app/                         // Next.js 13 app directory
//     (authenticated)/
//       orders/
//         page.tsx
//         [id]/
//           page.tsx

Feature co-location means moving or deleting a feature is a single directory operation. The index.ts barrel file exports only the public API, preventing deep imports into the feature internals from other features.

Ruby on Rails service layer organisation

# app/
#   models/          # ActiveRecord models (thin)
#   services/        # Business logic, one class per operation
#     orders/
#       place_order_service.rb
#       cancel_order_service.rb
#       bulk_refund_service.rb
#     payments/
#       stripe_charge_service.rb
#       refund_service.rb
#   queries/         # Complex AR queries, extracted from models
#     orders/
#       pending_orders_query.rb
#       orders_by_customer_query.rb
#   presenters/      # View-layer transformations
#     order_presenter.rb
#   serializers/     # API JSON serialization
#     order_serializer.rb
#   jobs/            # Background jobs
#     order_confirmation_email_job.rb

# Example: services follow the command pattern
class Orders::PlaceOrderService
  def initialize(order_params:, customer:, payment_gateway: Stripe)
    @order_params = order_params
    @customer = customer
    @payment_gateway = payment_gateway
  end

  def call
    order = Order.create!(@order_params.merge(customer: @customer))
    @payment_gateway.charge(order.total_cents, @customer.payment_method)
    order
  end
end

Extracting services, queries, and presenters from models keeps models thin. The orders/ subdirectory groups all order-related services together. Each service is named after the operation (verb + noun), not the resource (noun alone).

Use Cases

  • 01Large teams where engineers work on separate features; feature co-location minimises merge conflicts and makes ownership clear
  • 02Monorepos where packages/feature-name directories group related frontend and backend code
  • 03Domain-Driven Design implementations where directory structure mirrors Bounded Context boundaries
  • 04Open source projects where contributors need to find the right file quickly; a clear structure reduces time to first contribution

When Not to Use

  • //Do not prematurely adopt feature directories for a small project with 5-10 files; a flat src/ structure is simpler and easier to navigate
  • //Do not create deeply nested directories for single files; a directory with one file is often better expressed as a module-level export
  • //Do not use both feature-based and layer-based organisation in the same project; the inconsistency defeats the navigation benefit of either approach

Technical Notes

  • TypeScript path aliases (tsconfig.json paths: { "@features/*": ["src/features/*"] }) eliminate deep relative imports (../../../../shared/utils) and make refactoring directory structure less disruptive
  • Barrel files (index.ts re-exporting from a directory) improve import ergonomics but can cause circular dependency issues and slow TypeScript compilation in large codebases; use with awareness of these trade-offs
  • Next.js 13 app directory uses filesystem-based routing with co-located components, server components, and route handlers; the structure enforces a specific pattern for each route
  • Nx (monorepo tool) enforces module boundaries via lint rules: a feature library cannot import from another feature library, only from shared libraries. This prevents architectural drift in large monorepos