The Road to Next — your interactive course for Next.js with React

React Folder Structure Best Practices [2026]

Robin Wieruch

Organizing large React applications into folders and files is a topic that often sparks strong opinions. I found it challenging to write about this, as there isn’t a definitive “correct” approach. However, I frequently get asked how I structure my React projects, from small to large, and I’m happy to share my approach.

After implementing React applications for over a decade now, I want to give you a breakdown on how I approach this matter for my personal projects, for my freelance projects, for my React courses, and for my React SaaSs. We’ll walk through it step by step, and you decide what makes sense to you and how far you want to push it. So let’s get started.

Single React file

The first step follows the sentiment: One file to rule them all. Most React projects start with a src/ folder and one src/[name].(js|ts|jsx|tsx) file where you will find something like an App component. At least that’s what you get when you are using Vite for creating a client-side React application. If you are using a framework like Next.js for server-driven React, you will start with a src/app/page.js file.

Here we see an example of a function component which just renders JSX in a single file:

tsx
const App = () => {
  const title = "React";

  return (
    <div>
      <h1>Hello {title}</h1>
    </div>
  );
};

export default App;

As this component evolves with additional features, it naturally increases in size, making it necessary to break it down into smaller, independent React components. In this case, we are extracting a React list component along with another child component from the App component, where we need to pass data down through props:

tsx
const projects = [
  {
    id: "1",
    name: "Internal Tools",
  },
  {
    id: "2",
    name: "Mobile App",
  },
];

const ListItem = ({ project }) => (
  <li>
    <span>{project.id}</span>
    <span>{project.name}</span>
  </li>
);

const List = ({ list }) => (
  <ul>
    {list.map((project) => (
      <ListItem key={project.id} project={project} />
    ))}
  </ul>
);

const App = () => <List list={projects} />;

When starting a new React project, it’s acceptable to have multiple components in one file. In larger applications, this can still be tolerable if the components are closely related. However, as your project grows, a single file will eventually become insufficient. At that point, you’ll need to transition to using multiple files.

Multiple React files

The second step follows the sentiment: Multiple files to rule them all. Take for example our previous List and ListItem components: Rather than having everything in one file, we can split these components up into multiple files. You decide how far you want to take it here. For example, I would go with the following folder structure:

text
- src/
--- app.js
--- list.js

While the list.js file would have the implementation details of the List and ListItem components, it would only export the List component from the file as API:

tsx
const ListItem = ({ project }) => (
  <li>
    <span>{project.id}</span>
    <span>{project.name}</span>
  </li>
);

const List = ({ list }) => (
  <ul>
    {list.map((project) => (
      <ListItem key={project.id} project={project} />
    ))}
  </ul>
);

export { List };

Next the app.js file can import the List component and continue using it:

tsx
import { List } from './list';

const projects = [ ... ];

const App = () => <List list={projects} />;

If you would take this one step further, you could also extract the ListItem component into its own file and let the List component import the ListItem component:

text
- src/
--- app.js
--- list.js
--- list-item.js

However, as said before, this may take it too far, because at this point in time the ListItem component is tightly coupled to the List component and not reused anywhere else. Therefore it would be better to leave it in the src/list.js file.

Read More
Why kebab-case instead of PascalCase for files

I follow the rule of thumb that whenever a React component becomes a reusable React component, I split it out as a standalone file, like we did with the List component, to make it accessible for other React components.

Also note that I’m using .js extensions throughout this post for the sake of simplicity. In modern React codebases, you’ll almost always see .tsx files (TypeScript with JSX) instead, since TypeScript has become the default for serious React work. The folder structure stays the same either way.

From files to folders in React

From here, it becomes more interesting yet also more opinionated. Every React component grows in complexity eventually. Not only because more logic is added (e.g. more JSX with conditional rendering or logic with React Hooks and event handlers), but also because there are more technical concerns like styles, tests, constants, utilities, types. All of these things could potentially be extracted into their own files.

A naive approach would be to add more files next to each React component. For example, let’s say every React component has a test and a style file:

text
- src/
--- app.js
--- app.test.js
--- app.css
--- list.js
--- list.test.js
--- list.css

One can already see that this doesn’t scale well, because with every additional React component in the src/ folder we will lose more sight of every individual component. That’s why I like to have one folder for each React component:

text
- src/
--- app/
----- index.js
----- component.js
----- test.js
----- style.css
--- list/
----- index.js
----- component.js
----- test.js
----- style.css

While the new style and test files implement styling and testing for each local component respectively, the new component.js file holds the actual implementation logic of the component.

If you’re using Tailwind CSS (or another utility-first or CSS-in-JS approach), your component folders may not include a style.css file at all. The styles live as className strings inside component.js, so you have one less file per component. The shape of the folder still holds.


What’s missing an explanation is the new (yet optional) index.js file which represents the public interface (i.e. public API) of the folder (i.e. module) where everything gets exported that’s relevant to the outside world. Many know this file under the term barrel file, which is usually not recommended in JavaScript, because it makes tree shaking harder for bundlers.

However, if you don’t just re-export everything from the folder, but only the public API, then it can be a good practice, because you don’t leak implementation details (e.g. styles) to the outside world. In other words, you would only be allowed to import from the index.js file and not from the component.js or style.css file.

For example, for the List component, the src/list/index.js file would look like this:

tsx
export * from "./list";

If you want to be more specific to avoid leaking implementation details, you can also export the List component directly:

tsx
import { List } from "./list";

export { List };

The App component in its component.js file can still import the List component the following way:

tsx
import { List } from "../list/index.js";

We can also omit the /index.js, because it’s the default for most bundlers in JavaScript:

tsx
import { List } from "../list";

Anyway, barrel files are getting out of fashion in JavaScript, because they make tree shaking harder for bundlers. So you can also just import the List component directly from the src/list/list.js file and omit the src/list/index.js file.


On another note, the naming convention of the presented files is opinionated as well: For example, test.js can become spec.js or style.css can become styles.css if a pluralization of files is desired. Moreover, if you are not using CSS but something like CSS modules, your file extension may change from style.css to style.module.css too.

Read More
Learn more about CSS Style in React

Once you get used to this naming convention of folders and files, you can just fuzzy search for list component” or “app test” in your IDE for opening each file.

But here I admit, in contrast to my personal taste of concise file names, that people often prefer to be more redundant with their folder/file names:

text
- src/
--- app/
----- index.js
----- app.js
----- app.test.js
----- app.style.css
--- list/
----- index.js
----- list.js
----- list.test.js
----- list.style.css

Anyway, if you collapse all component folders, regardless of the file names, you have a concise folder structure with all the hidden implementation details of your components:

text
- src/
--- app/
--- list/

If there are more technical concerns for a component, for example you may want to extract custom React hooks, types (e.g. TypeScript definitions), stories (e.g. Storybook), utilities (e.g. helper functions), or constants (e.g. JavaScript constants) into dedicated files, you can scale this approach horizontally within the component folder:

text
- src/
--- app/
----- index.ts
----- component.ts
----- test.ts
----- style.css
----- types.ts
--- list/
----- index.ts
----- component.ts
----- test.ts
----- style.css
----- hooks.ts
----- stories.ts
----- types.ts
----- utils.ts
----- constants.ts

If you decide to keep your List component smaller by extracting the ListItem component into its own file, then you may want to try the following folder structure:

text
- src/
--- app/
----- index.js
----- component.js
----- test.js
----- style.css
--- list/
----- index.js
----- component.js
----- test.js
----- style.css
----- list-item.js

Once the ListItem component grows in size and complexity, you can go one step further by giving the component its own nested folder with all other technical concerns:

text
- src/
--- app/
----- index.js
----- component.js
----- test.js
----- style.css
--- list/
----- index.js
----- component.js
----- test.js
----- style.css
----- list-item/
------- index.js
------- component.js
------- test.js
------- style.css

From now on, it’s important to be cautious about nesting your components too deeply. My rule of thumb is to avoid nesting more than two levels. For instance, the list and list-item folders are fine as they are, but there shouldn’t be another nested folder inside the list-item folder. That said, there can always be exceptions to this rule.

Read More
Learn more about Libraries in React

After all, if you are not going beyond small React projects, this is in my opinion the way to go to structure your React components.

Technical Folders in React

The next step will help you to structure midsize React applications, because it separates React components from reusable React features such as custom hooks and context, but also none React related features like helper functions (here utils/).

Take the following folder structure with another separating folder as a baseline:

text
- src/
--- components/
----- app/
------- index.js
------- component.js
------- test.js
------- style.css
----- list/
------- index.js
------- component.js
------- test.js
------- style.css

All previous React components got grouped into a new components/ folder. This gives us another vertical layer for creating folders for other technical categories.

Read More
Learn more about Custom React Hooks

For example, at some point you may have reusable React Hooks that can be used by more than one component. So instead of coupling a hook tightly to a component, you can put the implementation of it in a dedicated folder to share it with all components:

text
- src/
--- components/
----- app/
------- index.js
------- component.js
------- test.js
------- style.css
----- list/
------- index.js
------- component.js
------- test.js
------- style.css
--- hooks/
----- use-click-outside.js
----- use-scroll-detect.js

This doesn’t mean that all hooks should end up in this folder though. React Hooks which are still only used by one component should remain in the component’s file or a hooks.js file next to the component in the component’s folder. Only reusable hooks end up in the new hooks/ folder.

If there are more files needed for one hook, you can change it into a folder again. You can also mix and match folder/file structures (not only applicable for the hooks folder), because whereas one hook may only need a file, another hook may need a folder:

text
- src/
--- components/
----- app/
------- index.js
------- component.js
------- test.js
------- style.css
----- list/
------- index.js
------- component.js
------- test.js
------- style.css
--- hooks/
----- use-click-outside/
------- index.js
------- hook.js
------- test.js
----- use-scroll-detect.js

The same strategy may apply if you are using React Context in your React project. Because context needs to get instantiated somewhere, a dedicated folder/file for it is a best practice, because it needs to be accessible by many React components eventually:

text
- src/
--- components/
----- app/
------- index.js
------- component.js
------- test.js
------- style.css
----- list/
------- index.js
------- component.js
------- test.js
------- style.css
--- hooks/
----- use-click-outside.js
----- use-scroll-detect.js
--- context/
----- session.js

From here, there may be other utilities which need to be accessible from your components/ folder, but also from the other new folders such as hooks/ and context/.

For miscellaneous utilities, I usually create a utils/ folder. The name is up to you. Again it’s the principle of making logic available to other code in the project which drives this technical divide:

text
- src/
--- components/
----- app/
----- list/
--- hooks/
----- use-click-outside.js
----- use-scroll-detect.js
--- context/
----- session.js
--- utils/
----- error-tracking/
------- index.js
------- util.js
------- test.js
----- format/
------- date-time/
--------- index.js
--------- util.js
--------- test.js
------- currency/
--------- index.js
--------- util.js
--------- test.js

Take for instance the date-time/index.js file’s implementation details as an example:

tsx
export const formatDateTime = (date) =>
  new Intl.DateTimeFormat("en-US", {
    year: "numeric",
    month: "numeric",
    day: "numeric",
    hour: "numeric",
    minute: "numeric",
    second: "numeric",
    hour12: false,
  }).format(date);

export const formatMonth = (date) =>
  new Intl.DateTimeFormat("en-US", {
    month: "long",
  }).format(date);

Fortunately JavaScript’s Intl API gives us methods for date conversions. However, instead of using the API directly in my React components, I like to have a util for it, because only this way I can guarantee that my components have only a little set of actively used date formatting options available for my application.

Now it’s possible to import each date formatting function individually:

tsx
import { formatMonth } from "../../utils/format/date-time";

const month = formatMonth(new Date());

But I prefer it as an encapsulated module with a public API, which follows the following import strategy:

tsx
import * as dateTimeUtil from "../../utils/format/date-time";

const month = dateTimeUtil.formatMonth(new Date());

It may become difficult to import things with relative paths. Therefore I’d always opt-in into aliases with absolute imports. Afterward, your import may look like the following:

tsx
import * as dateTimeUtil from "@/utils/format/date-time";

const month = dateTimeUtil.formatMonth(new Date());

Opponents of barrel files may argue that this is a barrel file, because it re-exports everything from the folder. However, I see it as a public API for the folder, because it only exports the public API of the folder and not all the implementation details.

After all, I like this technical separation of concerns, because it gives every folder a dedicated purpose and it encourages sharing functionality across the React application. You can always adapt this structure to your needs, for example making the utils structure more fine-grained compared to the one from above:

text
--- utils/
----- error-tracking/
------- index.js
------- util.js
------- test.js
----- format/
------- date-time/
--------- date-time/
----------- index.js
----------- util.js
----------- test.js
--------- date/
----------- index.js
----------- util.js
----------- test.js
--------- time/
----------- index.js
----------- util.js
----------- test.js
------- currency/
--------- index.js
--------- util.js
--------- test.js

Other technical folders you’ll see

Once you start reading other React codebases, you’ll run into folder names that aren’t covered above. Here’s the vocabulary I see most often, with the meaning that has become roughly standard:

FolderWhat it usually holds
lib/Pre-configured wrappers around third-party libraries (an axios instance, a Firebase init, a configured client)
api/Centralized API client code when many features share the same endpoints
stores/Global state (Redux, Zustand, Jotai stores)
config/Environment variables, app-level constants, runtime configuration
types/Shared TypeScript types and interfaces
providers/Context provider components, often paired with context/
layouts/Top-level layout components like sidebar, navbar, container
assets/Static files: images, fonts, icons
utils/ vs helpers/Some teams split these: utils/ is generic and copy-pasteable across projects, helpers/ is project-specific
routes/ or router/Route configuration (alternative to pages/)
testing/Centralized mocks, test utilities, test factories

You don’t need all of these. Pick the ones that map to actual concerns in your project. When in doubt, start with components/, hooks/, and one of utils/ or lib/, then add more as the codebase tells you it needs them.

Please see the proposed folder structure as a structural guideline and not a naming convention. The naming of the folders and files is up to you.

Feature Folders in React

The next step will help you to structure large React applications, because it separates specific feature related components from generic UI components. While the former are often only used once in a React project, the latter are UI components which are used by more than one component.

I’ll focus on components to keep the example concise, but the same principles can be applied to all the technical folders mentioned earlier. Consider the following folder structure as an example. While it may not fully illustrate the scope of the issue, I trust the point will be clear:

text
- src/
--- components/
----- list/
----- input/
----- button/
----- checkbox/
----- radio-button/
----- dropdown/
----- profile/
----- avatar/
----- project-form/
----- project-list/
----- customer-form/
----- customer-detail/
----- contact-card/
----- contact-list/

The point: There will be too many components in your components/ folder (or any other technical folder) eventually. While some of them are reusable (e.g. Button), others are more feature related (e.g. ProjectList).

From here, I’d use the components/ folder only for reusable components (e.g. UI components). Every other component should move to a respective feature folder. The names of the folders are up to you, but I like to use the feature name as the folder name:

text
- src/
--- features/
----- user/
------- profile/
------- avatar/
----- project/
------- project-form/
------- project-list/
----- customer/
------- customer-form/
------- customer-detail/
----- contact/
------- contact-card/
------- contact-list/
--- components/
----- list/
----- input/
----- button/
----- checkbox/
----- radio-button/
----- dropdown/

A note on naming: I keep folder and component names singular throughout. features/customer, not features/customers. customer-list, not customers-list. Even when a component renders a collection (a list of customers), the component itself is still one thing, so its folder name stays singular. The same applies to nested children: customer-list-item rather than customer-list-items. The exception is when a folder or file genuinely holds a collection: top-level folders like features/, components/, hooks/ are plural because they hold many of those things, and bundle files like types.ts, hooks.ts, stories.ts, utils.ts, constants.ts are plural because each of those files contains multiple definitions.

If one of the feature components (e.g. ProjectForm, CustomerForm) need access to a shared Checkbox, Radio or Dropdown component, it imports it from the reusable UI components folder. If a domain specific ProjectList component needs an abstracted List component, it imports it as well.

Furthermore, if a util from the previous section is tightly coupled to a feature, then move the util to the specific feature folder. The same may apply to other folders (e.g. hooks, context) which were previously separated by technical concern:

text
- src/
--- features/
----- user/
------- profile/
------- avatar/
----- project/
------- project-form/
------- project-list/
----- customer/
------- customer-form/
------- customer-detail/
------- utils/
--------- address/
----------- index.js
----------- util.js
----------- test.js
----- contact/
------- contact-card/
------- contact-list/
------- utils/
--------- phone/
----------- index.js
----------- util.js
----------- test.js
--- components/
--- hooks/
--- context/
--- utils/
----- format/
------- date-time/
--------- index.js
--------- util.js
--------- test.js

Whether there should be an intermediate utils/ folder in each feature folder is up to you. You could also leave out the folder and put the address/ folder directly into customer/. However, this may be confusing, because address formatting should be marked somehow as a util and not as a React component. So you could also go further with this structure for single feature folders by adding technical folders to them:

text
----- customer/
------- components/
--------- customer-form/
--------- customer-detail/
------- utils/
--------- address/
----------- index.js
----------- util.js

There is lots of room for your or your team’s personal touch here. After all, this step is just about bringing the features together which allows teams in your company to work on specific features without having to touch files across the project. How far you nest folders and where you separate technical concerns is up to you.

text
- src/
--- features/
----- feature-one/
------- technical-concern-one/
------- technical-concern-two/
------- ... // <--- maybe more technical concerns
----- feature-two/
------- technical-concern-one/
------- technical-concern-two/
------- ... // <--- maybe more technical concerns
--- components/
--- hooks/
--- context/
--- utils/
... // <--- maybe more globally shared technical folders

The big picture from above is to separate feature related components from reusable components and to separate technical concerns from feature related components.

Promoting Utils to the Shared Layer

In the previous step, I tucked the address/ util inside features/customer/utils/ and the phone/ util inside features/contact/utils/. That’s the right move when the util is genuinely tied to one feature. But the moment a second feature needs the same logic, things change.

Suppose the project feature also wants to format addresses, because every project has a site location. Now address/ is no longer tightly coupled to customer; it’s coupled to two features. The right move is to promote it back up to the shared utils/ folder at the top level:

text
- src/
--- features/
----- user/
------- profile/
------- avatar/
----- project/
------- project-form/
------- project-list/
----- customer/
------- customer-form/
------- customer-detail/
----- contact/
------- contact-card/
------- contact-list/
------- utils/
--------- phone/
--- components/
--- hooks/
--- context/
--- utils/
----- format/
------- address/
--------- index.js
--------- util.js
--------- test.js
------- date-time/
--------- index.js
--------- util.js
--------- test.js

The address util now lives at utils/format/address/, available to any feature that needs it. The phone util stays inside the contact feature, because so far only contact uses it. The day a second feature needs phone formatting too, it gets promoted the same way.

This kind of promotion happens often as a project grows, and it works in the other direction as well. Sometimes a util that started shared turns out to be specific to one feature after all, and gets demoted into that feature’s folder. The rule of thumb is simple: if exactly one feature uses a util, it lives inside that feature; once two or more features need it, it moves up to the shared layer.

The same logic applies to hooks, context, and components. A hook that only the project feature uses lives inside features/project/hooks/. The day the customer feature needs it too, it moves up to the top-level hooks/ folder. The whole purpose of the technical folders at the top level is to be the home for things that genuinely cross feature boundaries.

Boundaries between Features in React

Once you have feature folders in place, the question isn’t where things live, but how they’re allowed to talk to each other. Without rules, feature folders silently turn into another flat namespace where everything imports from everywhere. The folder structure looks fine, but the dependency graph is a mess.

Here are the rules I follow:

Code flows in one direction. From shared utilities (in components/, hooks/, utils/) into features, and from features into pages. Never the other way around. A reusable component in components/ should never reach into features/project/ to do its job. If you find yourself doing that, the thing in components/ probably belongs inside the feature, not outside it.

Features don’t import from each other. If features/project/ needs something from features/customer/, that’s a signal that either the shared piece actually belongs one layer up, or the two features should be composed at the page level rather than coupled directly. Keeping features independent is what makes them removable.

Each feature has a public API. That is the index.js file we already covered. Outside code imports from features/project (the public surface), never from features/project/components/project-list/internals. Whatever isn’t exported from the public API is implementation detail and can be reorganized freely.

A simple test for whether your boundaries hold: pick one feature folder, imagine deleting it, and ask yourself how many other folders break. If the answer is “everything”, your boundaries leaked. If the answer is “the pages that compose it, plus a clean error from anything that referenced its public API”, you’re in good shape.

This is the same idea as packages in a monorepo, just expressed as folders inside one project. Once your application gets big enough, those feature folders sometimes do graduate into actual packages, which the later sections of this post will cover.

Domain Folders in React

Once your features/ folder grows past a handful of entries, you’ll notice that some features cluster together. project/, customer/, contact/ all live on the workspace side of the application. user/, tenant/, role/ live on the platform side. comment/, space/ live on the content side. Twenty flat feature folders is hard to navigate; three domain groups of seven features each is much easier.

The fix is to introduce a domains/ folder that groups features by business area:

text
- src/
--- domains/
----- workspace/
------- features/
--------- project/
--------- customer/
--------- contact/
----- core/
------- features/
--------- user/
--------- tenant/
----- cms/
------- features/
--------- comment/
--------- space/
--- components/
--- hooks/
--- context/
--- utils/

Each domains/[name]/features/ folder follows the same shape as the feature folders earlier: features inside it own their components, hooks, queries, actions, and so on. The domain folder itself doesn’t hold code; it’s a grouping layer above features.

The boundary rules from the previous step carry up. Features inside workspace/ don’t import directly from features inside cms/. If they need to, the shared piece moves to core/ (which everyone is allowed to depend on) or to the top-level components/, hooks/, utils/ folders. The “delete a feature in one folder” test extends one level: “delete a domain in one folder”. If removing cms/ breaks something in workspace/, your domains leaked.

This doesn’t kick in for small or medium apps. It’s the natural step right before extracting shared code into separate packages, which is what the next section covers.

Package Folders in React

So far everything has lived inside src/, including the shared components/, hooks/, and utils/ at the top level. As your application grows, or as you want strict boundaries between your shared building blocks and the rest of the app, it makes sense to extract that shared code into its own package alongside src/:

text
- src/
--- domains/
----- workspace/features/...
----- core/features/...
--- app/
--- ...
- packages/
--- shared/
----- src/components/
----- src/hooks/
----- src/utils/
- package.json

Each entry under packages/ is its own module with its own package.json. Your app inside src/ imports from those packages by name (@yourorg/shared or similar) instead of from a relative path. The boundary between “shared building blocks” and “application” is now a package boundary, not just a folder boundary.

This is more setup than most apps need on day one. But once you want clear versioning of your UI library, want to enforce that the app cannot reach into shared internals, or want to share these building blocks across multiple apps, packages give you a stronger boundary than folders alone.

Configuration packages live here too. In a real codebase, you’ll often see packages/typescript-config/ for shared tsconfig presets, packages/vitest-config/ for testing setup, and packages/eslint-config/ for linting rules. Each is a tiny package whose only job is to ship a config file that other packages and apps import.

This is the natural step right before splitting into multiple apps, which is what the next section covers.

App Folders in React

When one project needs to ship multiple deployable web applications, an admin tool, a customer-facing app, a CMS, all built on the same domain logic, the single src/ folder no longer fits. The fix is to introduce an apps/ folder at the top level where each child is a complete, independently deployable application, and to promote your domain folders out of src/ into a top-level domains/ folder so multiple apps can share them:

text
- apps/
--- web-admin/
--- web-workspace/
--- web-cms/
- domains/
--- workspace/src/features/...
--- core/src/features/...
--- cms/src/features/...
- packages/
--- shared/src/...
--- typescript-config/
--- vitest-config/
- package.json

This is what people mean by “monorepo”: one repository, multiple apps, shared domain and utility packages. Each entry under apps/ has its own routing, layout, and configuration, and pulls in the domain packages it needs. Each entry under domains/ holds the business logic for one slice of the product. Each entry under packages/ is a reusable building block.

The dependency rules from the boundaries section now express themselves as package dependencies:

  • Apps can depend on domains and packages. Apps don’t depend on each other.
  • Domains can depend on packages. Most domains can also depend on a foundational domain (often called core or platform) that owns shared entities like users, tenants, and roles. Other than that, domains don’t depend on each other directly.
  • Packages don’t depend on anyone. They are the foundation everything else builds on.

The “delete a feature in one folder” test from the boundaries section now extends to “delete a domain package”, and the answer should still be the same: clean errors at the public API boundary, no leaked internals across packages.

This is the structure I’m using on a real production project right now. It scales: you can add a new app without touching domains, refactor a domain without touching other domains, and swap out a package without touching apps. The earlier single-app structure didn’t disappear; it lives inside each domain (features/, components/, hooks/, utils/) and inside each app (app/, pages/), just one level down.

Bonus: Page driven Project Structure

Eventually you will have multiple pages in your React application. If you are using a framework like Next.js, you will have a app/ folder where you put your page.tsx files for file based routing. However, if you are using a client-driven React application (e.g. React + Vite), you may want to structure your project around a pages/ folder as well, because pages are the entry points for users to interact with your application:

text
- src/
--- pages/
--- features/
--- components/
--- hooks/
--- context/
--- utils/

In a Next.js project, the app folder is the pages folder. The following structure shows an example of how you could structure it for a CRUD application around a project feature:

text
- src/
--- app/
----- page.tsx
----- projects/
------- page.tsx
------- [projectId]
--------- page.tsx
--- features/
----- project/
------- project-list/
----- contact/
------- contact-list/
--- components/
----- list/
--- hooks/
--- context/
--- utils/

In this example, the user would be able to go to a /projects page to see a list of projects and to a /projects/[projectId] page to see a single project with its contacts. Whereas the ProjectList would be used on the /projects page, the ContactList would be used on the /projects/[projectId] page. Both of these list components would reuse the List component from the components/ folder.

One could make a whole discussion about the above folder structure, because there are various things to consider. Don’t take the following things as granted, but as a starting point for a discussion:

  • Should the contact feature folder be nested in the project feature folder?
    • Yes, it could be nested if the contact feature is only used by the project feature.
    • No, it shouldn’t be nested if the contact feature is used by multiple features.
      • For example, if there are more features like a customer/ or partner/ where contacts also belong, the contact feature would be a shared feature across multiple features.
  • Should the list component be nested in the project feature folder?
    • Yes, it could if the project feature is the only feature using the list component.
    • No, it shouldn’t if the list component is used by multiple features.
      • In our example above, the list component is used by the project feature and the contact feature. Therefore it should be a shared component across multiple features.
  • There are React frameworks which allow private folders in the pages/ (in Next.js app/) folders. If this is the case, should the project/ feature be moved as private folder into the pages/projects/ folder?
    • Yes, it could be moved if the project feature is only used by the /projects page.
      • But I’d always advice against it, because it makes 1) the feature folder structure inconsistent. If you have a feature folder structure, then keep it consistent across the project. And 2) it makes the feature folder structure less flexible. If you want to reuse the project feature in another page later, you would have to move it out of the private folder again.

Outlook: Inside a feature at scale

Zooming back in on one feature inside a domain package, here’s what the shape looks like at production scale:

text
domains/workspace/src/features/project/
--- types.ts
--- enums/
----- project-state.ts
--- queries/
----- get-project.ts
----- get-projects.ts
--- actions/
----- upsert-project-action.ts
----- delete-project-action.ts
--- components/
----- project-upsert-form.tsx
----- project-delete-button.tsx
----- project-table/
------- columns.tsx
------- table.tsx
------- index.ts
--- relations/
----- customer/
------- actions/
--------- add-customer-to-project-action.ts
------- components/
--------- customer-for-project-manage-button.tsx
----- user/
------- queries/
--------- get-projects-by-user.ts

A feature owns its full slice: data fetching (queries/), mutations (actions/), UI (components/), types and enums, and any feature-coupled hooks or utils. Naming follows kebab-case throughout: a -action.ts suffix for server actions, a get- prefix for queries, and descriptive component names like project-upsert-form.tsx. Nothing exotic, just the same conventions from earlier steps carried up to the package level.

The relations/ subfolder is the interesting one. It captures the unavoidable reality that features sometimes need to reach across boundaries: a project needs to know its customer, a user needs to see their projects. Instead of letting that coupling leak into either feature’s main folder (which would break the “delete a feature in one folder” test from the boundaries section), the cross-feature code lives in a clearly labeled relations/ subfolder, mirrored by the related feature name. project/relations/customer/ tells you exactly what kind of coupling exists and where to find it.

You don’t need to reach for a monorepo on day one. But once you do, the structure you’ve been using inside src/ for a single app scales up almost unchanged: features become packages, technical folders become shared packages, pages stay in apps. The steps above are the same steps, just one level down inside a feature that owns its full slice.


Having all this written, I hope it helps one or the other person or team structuring their React project. Keep in mind that none of the shown approaches is set in stone. In contrast, I encourage you to apply your personal touch to it. Since every React project grows in size over time, most of the folder structures evolve very naturally as well. Hence the step-by-step process to give you some guidance if things get out of hand.

Never Miss an Article

Join 50,000+ developers getting weekly insights on full-stack engineering and AI.

AI Agentic UI Architecture React Next.js TypeScript Node.js Full-Stack Monorepos Product Engineering
Subscribe on Substack

High signal, low noise. Unsubscribe at any time.