Advanced Next.js File Architecture

By Aurelian Spodarec
Thumbnail

Over the years, working with React, Next.js, or really any tech or language, I’ve noticed a recurring challenge—especially on the front-end side—when it comes to establishing solid architecture.

Article writing in progress.

Everyone claims to care about architecture, but when I look at the code, it’s far from ideal. Developers might talk about principles like atomic design, but rarely implement them. They understand that separation of concerns is important, yet they overdo it, leading to a tangled, spiderweb-like codebase where it’s difficult to trace where things belong, or to whom they belong—creating confusion.

In this article, I’m going to share a proven architectural pattern for Next.js. While the focus is on Next.js, these principles can easily be applied outside of this framework and to other types of development. I just happen to be working with front-end a lot, so I’ll use that as my example.

For a quick preview of what I’ll be discussing, check out my GitHub example: GitHub Example. The focus here is on the architecture concept, not fancy features.

I may split this article into two parts—let’s start with basic folder structure and component organization.

The Problem

When I work with other codebases, I often see basic structures like this:

/src
│
├─ /app
│   ├─ home
│   │   └─ page.tsx
│   ├─ about
│   │   └─ page.tsx
│   │
│   └─ layout.tsx
│
└─ /components
    ├─ Home.tsx
    ├─ Modal.tsx
    ├─ About.tsx
    ├─ Sidebar.tsx
    ├─ ProductCard.tsx
    ├─ Footer.tsx
    ├─ Header.tsx
    └─ Button.tsx

Even when the structure improves slightly, it often doesn’t stray too far from this pattern.

You might even try to add a global folder like "Interfaces" to store TypeScript interfaces and types, or a utils folder for your API functions, utilities, and so on. But soon enough, things become scattered, and it gets harder to figure out what belongs where, or how to locate specific files.

A Better Way: TREE Style Pattern

As the name suggests, the best way to manage folders, files, and components is by following the "TREE" pattern.

What does that mean? Think of it like a tree—branches grow, and each branch is home to different leaves. The leaves grow, and more branches appear for each leaf.

For example, you create a page called home, and the page needs a header and footer. These two components grow on the home branch, so you place them inside the home folder. Now, the home page uses the header and footer.

Now, let’s say you want to create a new page called about. Since the two pages are separate entities, you need to move the header and footer components up a level. That way, any page you create will be able to use the same components.

Albert Einstein would flip thinking of moving branches around—after all, in the real world, branches don’t just move on their own! But in the world of architecture, we have the power to bend time and space... well, at least our code structure.

Let's have a look at how you could structure a basic "website" folder.

Here’s an example:

/src
│
├─ /app
│   ├─ /(website)
│   │   ├─ /_components //<-- Both components will be used inside the (website)
│   │   │   ├─ /Footer
│   │   │   │   └─ index.tsx
│   │   │   └─ /Header
│   │   │       └─ index.tsx
│   │   │
│   │   ├─ /(pages)
│   │   │   ├─ /home
│   │   │   │   └─ page.tsx
│   │   │   ├─ /about
│   │   │   │   └─ page.tsx
│   │   │
│   │   └─ layout.tsx
│   │
│   └─ layout.tsx
│   
├─ /components

What About a Shared "Components" Folder?

Let’s say you need a button used in our header for a login button. We could, of course, put our button in the "global" components folder, but it doesn't make sense to do so.

We’ll put the Button component inside the (website) component folder and use it within that branch. You might think this is wrong, and you'd be right—except that the button, as it stands, isn’t being used anywhere else in the project.

Generally speaking, if a component is not going to be used elsewhere, then it should stay local to the branch that is being used. In this case, it would be used only inside the (website).

From experience, we know that the Button component will be used all over the app, but bear in mind this is for demonstration purposes—showcasing how we could actually reason on putting the Button in the global shared component, instead of just doing so because we always did.

/src
│
├─ /app
│   ├─ /(website)
│   │   ├─ /_components 
│   │   │   ├─ Button.tsx // <-- Added Button
│   │   │   ├─ /Footer
│   │   │   │   └─ index.tsx
│   │   │   └─ /Header
│   │   │       └─ index.tsx
│   │   │
│   │   ├─ /(pages)
│   │   │   ├─ /home
│   │   │   │   └─ page.tsx
│   │   │   ├─ /about
│   │   │   │   └─ page.tsx
│   │   │
│   │   └─ layout.tsx
│   │
│   └─ layout.tsx
│   
├─ /components

Again, I know it might look odd, but this is Tree pattern in action.

If the component needs to be used elsewhere in the future, we can move the branch up by one or two levels. The Button is governed by the (website) folder, so it can be imported anywhere inside that folder, but it can't be imported from the outside.

Note: There are exceptions to this rule that will be mentioned elsewhere.

Adding a Dashboard

/src
│
├─ /app
│   ├─ /(website)
│   │   ├─ /_components 
│   │   │   ├─ Button.tsx
│   │   │   ├─ /Footer
│   │   │   │   └─ index.tsx
│   │   │   └─ /Header
│   │   │       └─ index.tsx
│   │   │
│   │   ├─ /(pages)
│   │   │   ├─ /home
│   │   │   │   └─ page.tsx
│   │   │   ├─ /about
│   │   │   │   └─ page.tsx
│   │   │
│   │   └─ layout.tsx
│   │
│   ├─ /dashboard // <-- Added Dashboard
│   │   ├─ /_components 
│   │   │   ├─ /Header
│   │   │   │   └─ index.tsx
│   │   │   └─ /Sidebar
│   │   │       └─ index.tsx
│   │   │
│   │   ├─ /(pages)
│   │   │   ├─ /home
│   │   │   │   └─ page.tsx
│   │   │   ├─ /analytics
│   │   │   │   └─ page.tsx
│   │   │
│   │   └─ layout.tsx
│   │
│   └─ layout.tsx
│   
├─ /components

We’ve added a dashboard, and it again has components like header and sidebar that are only being used inside the dashboard layout.

Now, remember when we talked about the button being in the right place inside (website)? Well, we need the same button inside the Sidebar and Header of the dashboard.

This is also what we call progressive improvement—refactoring, iterations, and part of programming. Given that the Button is now a "shared" component across the Website and Dashboard, we can move it up a branch once again. Now, the component will be inside the app folder—not the global components folder, as there’s no need for that yet.

/src
│
├─ /app
│   ├─ /_components // -- Moved the button here
│   │  └─ Button.tsx
│   │   
│   ├─ /(website)
│   │   ├─ /_components 
│   │   │   ├─ /Footer
│   │   │   │   └─ index.tsx
│   │   │   └

─ /Header
│   │   │       └─ index.tsx
│   │   │
│   │   ├─ /(pages)
│   │   │   ├─ /home
│   │   │   │   └─ page.tsx
│   │   │   ├─ /about
│   │   │   │   └─ page.tsx
│   │   │
│   │   └─ layout.tsx
│   │
│   ├─ /dashboard
│   │   ├─ /_components 
│   │   │   ├─ /Header
│   │   │   │   └─ index.tsx
│   │   │   └─ /Sidebar
│   │   │       └─ index.tsx
│   │   │
│   │   ├─ /(pages)
│   │   │   ├─ /home
│   │   │   │   └─ page.tsx
│   │   │   ├─ /analytics
│   │   │   │   └─ page.tsx
│   │   │
│   │   └─ layout.tsx
│   │
│   └─ layout.tsx
│   
├─ /components

Notice how we moved the Button up the tree to a more general location? It’s now within _components at the root of the app/ folder. This is the power of tree-based thinking in architecture.

When you're ever tangled in the maze of folders, do not be troubled. Remember the mightly tree - its branches. Let its sacred pattern light your way, and you shall find the rightful place to lay your entity.

Bigger Project

/src
│
├─ /app
│   ├─ /_components // -- Moved the button here
│   │   
│   ├─ /(website)
│   │   ├─ /_components 
│   │   │   ├─ /Footer
│   │   │   │   └─ index.tsx
│   │   │   └ /Header
│   │   │       └─ index.tsx
│   │   │
│   │   ├─ /(pages)
│   │   │   ├─ /home
│   │   │   │   └─ page.tsx
│   │   │   ├─ /about
│   │   │   │   └─ page.tsx
│   │   │
│   │   └─ layout.tsx
│   │
│   ├─ /dashboard
│   │   ├─ /_components 
│   │   │   ├─ /Header
│   │   │   │   └─ index.tsx
│   │   │   └─ /Sidebar
│   │   │       └─ index.tsx
│   │   │
│   │   ├─ /(pages)
│   │   │   ├─ /home
│   │   │   │   └─ page.tsx
│   │   │   ├─ /analytics
│   │   │   │   └─ page.tsx
│   │   │
│   │   └─ layout.tsx
│   │
│   └─ layout.tsx
│   
├─ /components
│   ├─ Input.tsx
│   ├─ Accordion.tsx
│   ├─ Modal.tsx
│   ├─ Paginatino.tsx
│   ├─ Avatar.tsx
│   ├─ Card.tsx
│   └─ Button.tsx

Story Time

I worked at a company in 2022 where their repo had 275 folders—each containing two nearly identical modal components. They ended up with close to a thousand modal files, when all of them could’ve been refactored into 6–7 reusable components. I actually consolidated them in my spare time, but for political reasons I couldn’t share the fix. The CEO never realized the team simply lacked architectural know‑how. End of story.

Overseparation and Centralised Entities

As the proejct evovled, you might want to create a shared NPM package, maybe a shares JSON package with all desing tokens that are used withing Andoird, iPhone, Web etc... this is just the tip of architecturing. Everything is a Tree.

Conclusion

The key takeaway here is that architecture should grow just like a tree: it evolves based on needs. By organizing components in this way, you maintain flexibility, scalability, and clarity in your codebase. Once you fully embrace this approach, you’ll find that refactoring and moving things around becomes a far less daunting task.

Just like the branches on a tree, with a little bit of magic, your app's structure can change, grow, and adapt over time—without causing a mess! 🌳