Project Structure and TypeScript Conventions
BeginnerThe app directory is routing, not architecture. Deciding what belongs beside a route and what belongs in a shared layer is what keeps a Next codebase navigable past twenty routes.
Overview
Because any folder under `app` can hold components, it is tempting to put everything there — and equally tempting to put nothing there and keep a parallel `components` tree. Both extremes hurt. The convention that holds up is the one from the React track applied to routes: code used by exactly one route lives beside that route, code used by several moves up, and genuinely shared primitives sit outside `app` entirely. On top of that, a few TypeScript conventions make the framework check your routes rather than trusting you.
A Layout That Scales
What lives inside app, and what does not.
src/
app/
layout.tsx
page.tsx
problems/
page.tsx
loading.tsx
_components/ProblemCard.tsx // used by this route only
_lib/format.ts
(auth)/
login/page.tsx
register/page.tsx
api/
webhooks/razorpay/route.ts
components/
ui/Button.tsx // design-system primitives
shared/EmptyState.tsx
lib/
api-client.ts // transport
db.ts // server-only
auth.ts
features/
problems/ // domain logic used by several routes
queries.ts
actions.ts
types.ts
// The rule: app/ describes URLs. Anything that is not about a URL —
// a domain model, a query, a shared component — belongs outside it.
// _folders are never routable, which is what makes colocation safe.
// Prefix server-only modules and enforce it:
import 'server-only' // in lib/db.ts — a client import fails the build
import 'client-only' // in a browser-API moduleTypeScript That Catches Route Mistakes
Typed links, typed params, and the Next 15 async change.
// next.config.js — typed routes: a typo in a href fails the build
module.exports = { experimental: { typedRoutes: true } }
<Link href="/problmes"> // Type error: not a known route
// Page props
type Props = {
params: Promise<{ slug: string }> // Next 15: a Promise
searchParams: Promise<{ [k: string]: string | string[] | undefined }>
}
export default async function Page({ params, searchParams }: Props) {
const { slug } = await params
const { tab } = await searchParams
}
// In Next 14 these are plain objects. The async change is the single
// most common upgrade break — see the migration concept.
// Route handler typing
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> })
// Metadata
import type { Metadata } from 'next'
export const metadata: Metadata = { title: 'Problems' }
// Path aliases, so imports survive a move
// tsconfig.json
{ "paths": { "@/*": ["./src/*"] } }Conventions Worth Fixing Early
Decisions that are cheap now and expensive after fifty routes.
// 1. Where does data fetching live?
// A queries.ts per feature, imported by the server component.
// Not inline fetch calls scattered through pages.
export async function getProblem(slug: string) { ... }
// 2. Server actions in one place per feature, marked at the top:
// features/problems/actions.ts
'use server'
export async function createProblem(formData: FormData) { ... }
// 3. One barrel file per feature at most. Barrels at every level
// defeat tree shaking and create import cycles.
// 4. Decide on route naming: kebab-case folders, always.
// /learn/full-stack, not /learn/fullStack — URLs are lowercase and
// a case-sensitive deploy target will 404 what worked on Windows.
// 5. Keep next.config.js small. Redirects, image domains and headers
// belong there; business logic does not.
// 6. Do not put a component in app/ just because it is used by a
// page. Ask whether a second route would ever want it. If yes,
// it belongs outside app/ from the start.Key Points to Remember
- 1The app directory describes URLs; domain logic and shared components belong outside it
- 2Underscore-prefixed folders are never routable, which makes colocating route-specific code safe
- 3server-only and client-only turn a wrong import into a build failure instead of a leak
- 4typedRoutes makes a mistyped href a compile error, and Next 15 params must be awaited
- 5Settle route naming, a per-feature queries file and a per-feature actions file before the codebase grows
Interview Questions
Sign in to ask AriaWhat belongs inside the app directory and what does not?
What does the server-only package do?
What changed about params and searchParams in Next 15?
Ask Aria about Project Structure and TypeScript Conventions
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.