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)
import { DashboardPage } from '@/features/dashboard/dashboard-page'
export default function Page () {
return < DashboardPage />
}
Route modules should be thin - render feature entry components. import { NextRequest , NextResponse } from 'next/server'
import { getUsers } from '@/features/shared/api/users'
export async function GET ( request : NextRequest ) {
// 1. Parse input
const { searchParams } = new URL ( request . url )
const limit = Number ( searchParams . get ( 'limit' )) || 10
// 2. Call feature/shared logic
const users = await getUsers ({ limit })
// 3. Return response
return NextResponse . json ( users )
}
Route handlers stay thin: parse input, call feature logic, return response.
features/ (Domain Modules)
Feature Entry Component
Feature-Specific Logic
// 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)
API Client
Shared Utilities
// 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)
ui/ (Primitives)
blocks/ (Compositions)
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
web/components/blocks/modal.tsx
import { Dialog , DialogContent } from '@/components/ui/dialog'
export function Modal ({ isOpen , onClose , children } : Props ) {
return (
< Dialog open = { isOpen } onOpenChange = { onClose } >
< DialogContent > { children } </ DialogContent >
</ Dialog >
)
}
Rules:
Compose primitives from ui/
Reusable patterns
Avoid feature-specific state/logic
Dependency Direction
Follow these dependency rules to prevent coupling:
Features → Components
web/features/* may import from web/components/*// ✅ Good
import { Button } from '@/components/ui/button'
Features → Shared
web/features/* may import from web/features/shared/*// ✅ Good
import { useAuth } from '@/features/shared/auth'
Components ↛ Features
web/components/* should NOT import from web/features/*// ❌ Bad - keeps primitives tightly coupled
import { useProfile } from '@/features/profile/hooks'
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:
✅ Absolute Imports
❌ Deep Relative Imports
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:
✅ Named Exports
⚠️ Default Exports (Only for Route 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:
Feature Server Actions
Usage
// 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:
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.