Home/Learn/Next.js/Project Structure and TypeScript Conventions

Project Structure and TypeScript Conventions

Beginner
Foundations

The 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.

app/ is URLs; everything else lives outside
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 module

TypeScript That Catches Route Mistakes

Typed links, typed params, and the Next 15 async change.

typedRoutes, and awaiting params in Next 15
// 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.

queries.ts, actions.ts, kebab-case 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 Aria
1

What belongs inside the app directory and what does not?

Medium
2

What does the server-only package do?

Medium
3

What changed about params and searchParams in Next 15?

Medium

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.

Loading discussion…