A small Next.js project can begin with a few routes and components. As the application grows, the same folder may accumulate page components, API helpers, validation logic, database access, and unrelated UI elements.
Without a deliberate structure, changing one feature becomes increasingly risky. Feature boundaries help keep related code together while making dependencies easier to understand.
Start with the Framework’s Conventions
Next.js provides conventions for routing and special files. In the App Router, the app directory defines route segments and files such as page.tsx, layout.tsx, and loading.tsx.
A feature-oriented structure can coexist with these conventions:
src/
app/
dashboard/
page.tsx
projects/
page.tsx
features/
projects/
components/
actions/
validation.ts
types.ts
queries.ts
dashboard/
components/
queries.ts
components/
ui/
lib/
auth/
database/
This is one possible organization, not a required Next.js structure. Choose a layout that suits the application’s size and team.
Keep Route Files Focused
A route file should generally explain what the route renders and how it connects to the relevant feature.
import { ProjectList } from "@/features/projects/components/project-list";
export default function ProjectsPage() {
return <ProjectList />;
}
The feature component can handle the page’s presentation, while data access and validation remain in appropriately scoped modules.
Avoid turning every route file into a large collection of unrelated responsibilities.
Separate Client and Server Responsibilities
The App Router supports Server Components and Client Components. Their boundaries should be deliberate.
Server-side modules can handle operations that require trusted access to databases or server-only credentials. Client components should focus on interactions that require browser capabilities or client-side state.
Do not import database access code into a client component. Also avoid exposing server secrets through values passed to client-rendered components.
When a component needs interactivity, add a client boundary as narrowly as practical rather than converting an entire feature to client rendering unnecessarily.
Keep Data Access Near the Feature
A project feature might expose functions such as:
export async function getProjectsForUser(userId: string) {
// Query the data store using an authorized server-side path.
}
The example intentionally omits database-specific code. The important design point is that authorization must be enforced on the server, not inferred from a client-supplied user ID.
Keep queries focused on the data required by the feature. Avoid creating a generic abstraction for every possible query before there is a concrete need.
Treat Server Actions as Entry Points
Server Actions can simplify mutations, but they should not be treated as automatically trusted simply because they run on the server.
Validate incoming data, authenticate the caller, check authorization, and return a clear result. Organize actions near the feature they modify when that makes ownership easier to understand.
For larger applications, shared validation schemas can reduce inconsistencies between forms and server-side logic.
Avoid Both Extremes
Two organizational mistakes are common.
The first is putting nearly all code into one giant directory. The second is creating many layers and abstractions before the application needs them.
A useful structure makes it easy to answer three questions:
- Where does this feature’s code live?
- Which modules are allowed to depend on it?
- Which responsibilities are shared across the application?
If those answers are obvious, the structure is probably doing its job.
Conclusion
A maintainable Next.js application combines framework conventions with clear feature boundaries. Keep routes focused, separate server and client responsibilities, group related logic, and enforce authorization at trusted server-side entry points.
Good architecture is not about having the most folders. It is about making changes predictable as the application grows.