Skip to main content
The Web Project Conventions skill provides best practices for organizing Next.js applications with TypeScript and React. Learn the recommended project structure, naming conventions, and dependency patterns.

Overview

This skill covers:
  • Project structure - Where different types of code belong
  • Naming conventions - Files, components, functions, and variables
  • Import patterns - Absolute imports and avoiding circular dependencies
  • Dependency direction - Preventing coupling between features

Project Structure

Top-Level Directories

project-root/
├── docs/              # Project documentation and design specs
├── infra/             # Infrastructure-as-code (Bicep, Terraform)
├── web/               # ALL application code lives here
├── .github/           # GitHub workflows, skills, agents
├── .vscode/           # Workspace settings
└── .azure/            # Azure-specific config
All application code must live under web/. Do not add application code outside this folder.

Web Application Structure

web/
├── app/                      # Next.js App Router
│   ├── layout.tsx           # Root layout
│   ├── page.tsx             # Home page (/)
│   ├── api/                 # Route handlers
│   │   └── users/route.ts
│   └── dashboard/
│       └── page.tsx         # /dashboard

├── features/                 # Domain modules
│   ├── profile/
│   │   ├── profile-page.tsx        # Entry component
│   │   ├── components/             # Feature-specific components
│   │   ├── hooks/                  # Feature-specific hooks
│   │   └── utils.ts                # Feature-specific utilities
│   ├── auth/
│   └── shared/                     # Cross-feature concerns
│       ├── api/                    # API clients
│       ├── auth/                   # Auth utilities
│       └── utils/                  # Shared helpers

├── components/               # Reusable UI components
│   ├── ui/                  # Primitives (no domain logic)
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   └── dialog.tsx
│   ├── blocks/              # Compositions (reusable patterns)
│   │   ├── modal.tsx
│   │   └── header.tsx
│   └── lib/
│       └── utils.ts         # Includes cn() helper

└── package.json

Directory Guidelines

app/ (Routing)

app/dashboard/page.tsx
import { DashboardPage } from '@/features/dashboard/dashboard-page'

export default function Page() {
  return <DashboardPage />
}
Route modules should be thin - render feature entry components.

features/ (Domain Modules)

// web/features/profile/profile-page.tsx
import { Card } from '@/components/ui/card'
import { useAuth } from '@/features/shared/auth'
import { ProfileForm } from './components/profile-form'

export function ProfilePage() {
  const { user } = useAuth()
  
  return (
    <div>
      <h1>{user.name}'s Profile</h1>
      <Card>
        <ProfileForm user={user} />
      </Card>
    </div>
  )
}
Features own:
  • Domain components
  • Feature-specific hooks
  • Feature-specific utilities
  • State management (if using Zustand)

features/shared/ (Cross-Feature)

// web/features/shared/api/users.ts
export async function getUsers(params: GetUsersParams) {
  const response = await fetch('/api/users?' + new URLSearchParams(params))
  return response.json()
}

export async function createUser(data: CreateUserData) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  })
  return response.json()
}
Use features/shared/ for:
  • API clients used by multiple features
  • Auth utilities
  • Shared domain helpers
  • Server-only logic for API routes

components/ (Reusable UI)

web/components/ui/button.tsx
import { cn } from '@/components/lib/utils'

export function Button({ className, variant, ...props }: ButtonProps) {
  return (
    <button
      className={cn(
        "px-4 py-2 rounded-md",
        variant === "primary" && "bg-blue-600 text-white",
        className
      )}
      {...props}
    />
  )
}
Rules:
  • No domain logic
  • No data fetching
  • No feature-specific state
  • Generic and reusable

Dependency Direction

Follow these dependency rules to prevent coupling:
1

Features → Components

web/features/* may import from web/components/*
// ✅ Good
import { Button } from '@/components/ui/button'
2

Features → Shared

web/features/* may import from web/features/shared/*
// ✅ Good
import { useAuth } from '@/features/shared/auth'
3

Components ↛ Features

web/components/* should NOT import from web/features/*
// ❌ Bad - keeps primitives tightly coupled
import { useProfile } from '@/features/profile/hooks'
4

No Cross-Feature Imports

Avoid web/features/a/* → web/features/b/*
// ❌ Bad - features should not depend on each other
import { formatUserName } from '@/features/profile/utils'

// ✅ Good - promote to shared
import { formatUserName } from '@/features/shared/utils'

Naming Conventions

Files and Folders

Use kebab-case for all files and folders:
✅ Good
user-profile.tsx
use-auth.ts
format-date.ts

❌ Bad
UserProfile.tsx
useAuth.ts
formatDate.ts

Components and Types

Use PascalCase for components, types, enums, and interfaces:
// ✅ Good
export function UserProfile() { ... }
export interface User { ... }
export enum Role { ... }

// ❌ Bad
export function userProfile() { ... }
export interface user { ... }

Functions and Variables

Use camelCase for hooks, functions, and variables:
// ✅ Good
export function useAuth() { ... }
export function formatDate() { ... }
const userName = 'John'

// ❌ Bad
export function UseAuth() { ... }
export function FormatDate() { ... }
const UserName = 'John'

Import Patterns

Use Absolute Imports

Prefer absolute imports via the @/ alias:
import { Button } from '@/components/ui/button'
import { useAuth } from '@/features/shared/auth'
import { ProfilePage } from '@/features/profile/profile-page'
The @/ alias maps to the web/ directory.

Named Exports

Prefer named exports in shared modules:
// web/features/shared/utils/format.ts
export function formatDate(date: Date) { ... }
export function formatCurrency(amount: number) { ... }

// Import
import { formatDate, formatCurrency } from '@/features/shared/utils/format'
Benefits of named exports:
  • More stable refactors
  • Easier searchability
  • Better IDE autocomplete
  • Consistent import syntax
When default exports are required:
  • Next.js route modules (page.tsx, layout.tsx, etc.)
  • When a library explicitly requires them

Server Actions

Colocation

Prefer colocating server actions with the feature that owns the behavior:
// web/features/profile/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { updateUser } from '@/features/shared/api/users'

export async function updateProfile(formData: FormData) {
  // 1. Validate input
  const name = formData.get('name') as string
  
  // 2. Call shared logic
  await updateUser({ name })
  
  // 3. Revalidate
  revalidatePath('/profile')
}
Server actions should:
  • Validate input
  • Call feature/shared logic
  • Return structured results
  • Import shared utilities from web/features/shared/

Quality Checklist

Before completing any task:
  • All application code is under web/
  • Files follow kebab-case naming
  • Components/types follow PascalCase
  • Functions/variables follow camelCase
  • components/* does not import from features/*
  • Features use features/shared/* for cross-feature code
  • No direct feature-to-feature imports
  • Using @/ absolute imports (not relative ../../../)
  • Named exports for shared modules
  • Default exports only where required (route modules)

Skill Structure

.github/skills/web-project-conventions/
└── SKILL.md    # This overview

References

Use your editor’s “Go to Definition” feature to verify import paths are correct. If it can’t find the module, neither can the bundler.

Build docs developers (and LLMs) love