A good TypeScript project structure makes code easier to find, test, build, and change. This guide provides a reusable checklist for organizing Node.js, React, Next.js, and monorepo applications, with practical guidance for tsconfig inheritance, shared types, path aliases, generated code, build outputs, and review points that help a codebase remain maintainable as it grows.
Overview
There is no single correct TypeScript folder structure. A small command-line tool does not need the same boundaries as a multi-package product, and a Next.js application has framework conventions that should not be replaced without a clear reason. The useful goal is not maximum abstraction. It is predictable organization: developers should know where application code, reusable modules, tests, configuration, and generated files belong.
Start with a few durable boundaries:
- Source code: Put production code in a clearly named directory such as
src, unless a framework strongly favors another convention. - Tests: Keep tests beside the code they exercise or in a dedicated test directory, but apply one convention consistently.
- Configuration: Keep compiler, lint, test, and bundler configuration at the repository or package root.
- Build output: Write compiled files to a directory such as
distorbuild, and keep it out of source control. - Generated code: Separate generated API clients, database types, or schemas from hand-written code and document how to regenerate them.
These boundaries also make migration easier. If you are converting an existing JavaScript application, establish the folders and build process incrementally rather than moving every file at once. The JavaScript to TypeScript migration checklist can help you sequence that work.
Checklist by scenario
Small Node.js application or library
For a focused service, script, or package, avoid creating folders before you need them. A practical starting point is:
project/
├── src/
│ ├── index.ts
│ ├── config.ts
│ └── features/
├── test/
├── package.json
├── tsconfig.json
├── tsconfig.build.json
└── README.md
Keep the entry point thin. Put business logic in modules that can be imported and tested independently. If the application exposes HTTP routes, group route handlers, services, and data-access code by feature once those areas become substantial. Do not create an elaborate controller-service-repository hierarchy for a project that has only a few functions.
- Check that
srccontains source files only. - Check that tests do not import from compiled output.
- Check that the package entry points match the files actually produced by the build.
- Check that development commands and production commands use intentionally different settings when necessary.
React application
React projects often benefit from a feature-oriented structure once the component tree becomes difficult to navigate:
src/
├── app/
│ ├── App.tsx
│ └── providers.tsx
├── features/
│ ├── billing/
│ │ ├── components/
│ │ ├── api.ts
│ │ ├── types.ts
│ │ └── billing.test.ts
│ └── account/
├── components/
├── hooks/
├── lib/
├── styles/
└── main.tsx
Use features for code that belongs to one product area and shared folders for genuinely reusable code. A component used by one feature should usually stay inside that feature. Move it to a shared directory only when its API and responsibilities are clear.
Keep browser-only code separate from server or build tooling code. Avoid placing all TypeScript types in a global types folder by default; local types are easier to maintain when they live near the functions and components that use them.
Next.js application
Next.js projects should respect the routing convention selected by the application. With the App Router, a structure might look like:
app/
├── layout.tsx
├── page.tsx
├── dashboard/
│ ├── page.tsx
│ └── loading.tsx
└── api/
└── health/
└── route.ts
src/
├── components/
├── lib/
├── server/
└── types/
public/
next.config.ts
tsconfig.json
Keep route-specific files close to their routes, while placing reusable UI and data-access modules in src or another clearly documented location. Be explicit about server-only modules. A shared utility that imports server credentials should not be accidentally consumed by a client component.
For a broader reference, see this guide to TypeScript with Next.js project structure and type-safe data fetching.
Monorepo
A monorepo should make package ownership and dependency direction visible:
repo/
├── apps/
│ ├── web/
│ └── api/
├── packages/
│ ├── config/
│ ├── domain/
│ ├── ui/
│ └── validation/
├── tooling/
├── package.json
├── tsconfig.base.json
└── workspace configuration
Use apps for deployable applications and packages for independently consumed libraries. Shared packages should have narrow responsibilities and explicit public entry points. Avoid a catch-all package such as common that becomes a dumping ground for unrelated helpers.
Define dependency rules before the repository becomes large. For example, UI packages may consume domain types, while a domain package should not import application-specific UI code. Project references or package-level builds can reinforce those boundaries, but the exact setup should match the chosen build tools.
What to double-check
TypeScript configuration
Use a base configuration for shared compiler settings and package-specific configurations for build targets. A common pattern is:
// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true
}
}
// tsconfig.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src", "test"]
}
Review include, exclude, rootDir, outDir, module settings, and declaration output rather than copying a configuration without understanding it. The TypeScript tsconfig guide provides a useful reference for these choices.
Path aliases
Aliases can improve imports, but they must work in every environment that runs the code: the editor, TypeScript, tests, the bundler, and production. A paths entry in tsconfig does not automatically teach Node.js how to resolve an alias at runtime. Verify the complete toolchain before adopting aliases widely. Use the TypeScript path alias guide when debugging this boundary.
Runtime data and generated files
TypeScript types disappear at runtime. Validate data crossing an API, environment, file, or database boundary instead of assuming that a declared interface makes external data safe. Keep schemas near the boundary or in a dedicated validation package, and decide whether generated types are committed, generated during CI, or produced as part of a package build. The guides to runtime validation with Zod cover practical patterns for this decision.
Build and test boundaries
Decide whether tsc is responsible for type checking only, emitting JavaScript, or both. Bundlers and alternative build tools may handle transformation differently. Whatever the choice, keep a separate, repeatable type-check command and ensure tests exercise source code or built code intentionally rather than accidentally mixing both.
Common mistakes
- One global types folder: This often becomes a collection of unrelated declarations. Keep types close to their domain unless they are truly shared.
- Deep import paths: Importing internal files from another package couples consumers to implementation details. Expose stable package entry points instead.
- Aliases used as architecture: A short import does not create a dependency boundary. Document which layers may depend on one another.
- Mixing generated and hand-written code: Regeneration can overwrite manual changes or create confusing review noise. Separate the directories and mark generated files clearly.
- Ignoring build output: Accidentally publishing source maps, test files, or development-only modules can make packages harder to consume. Inspect the package contents before release.
- Overusing barrel files: Broad
index.tsexports can create circular dependencies and increase import coupling. Add public barrels deliberately. - Copying framework conventions blindly: A structure that suits Next.js may not suit a Node.js library. Start from runtime and deployment requirements.
When to revisit
Review the project structure before a seasonal planning cycle, a major framework or build-tool change, or a migration milestone. Revisit it when a folder has become a catch-all, when developers cannot identify ownership, when tests require unusual setup, or when a package must be reused outside its original application.
Use this short review checklist:
- Can a new contributor find the application entry point, test command, and build output quickly?
- Are feature boundaries clearer than they were during the last review?
- Do aliases, package exports, and generated files work in the editor, tests, builds, and production?
- Are runtime validation and environment configuration located at the correct boundaries?
- Do monorepo packages expose only the APIs that consumers should rely on?
- Can you remove a folder or configuration option without breaking an undocumented workflow?
Make structural changes in small, reviewable steps. Move one boundary, update imports and tests, run the type checker and build, then document the convention. A scalable TypeScript project structure is not a fixed diagram; it is a set of clear decisions that remains understandable as the application, toolchain, and team change.