Mastering Next.js App Router: Best Practices for Structuring Your Application

Thiraphat Phutson
7 min readSep 18, 2024

Next.js has solidified its position as one of the leading React frameworks, offering developers a seamless blend of performance, flexibility, and ease of use. With the introduction of the App Router, Next.js has taken another leap forward, providing a more intuitive and powerful routing mechanism. Whether you’re building a personal blog or a large-scale enterprise application, structuring your Next.js project effectively is crucial for maintainability, scalability, and developer experience.

In this article, we’ll delve into the best practices for structuring your Next.js application using the App Router, ensuring your project remains organized, efficient, and easy to navigate.

Table of Contents

  1. Understanding the Next.js App Router
  2. Project Structure Overview
  3. Organizing Pages and Routes
  4. Managing Components Effectively
  5. Styling Strategies
  6. State Management Best Practices
  7. Optimizing Performance
  8. Testing and Quality Assurance
  9. Deployment Considerations
  10. Conclusion

Understanding the Next.js App Router

Before diving into structuring, it’s essential to grasp what the App Router brings to the table. The App Router in Next.js enhances routing capabilities by introducing features like:

  • Nested Routing: Enables complex route hierarchies with nested layouts.
  • Dynamic Routes: Simplifies the creation of dynamic URLs.
  • Server Components: Allows rendering components on the server for better performance.
  • Enhanced Data Fetching: Offers improved methods for fetching data, reducing client-side load.

Understanding these features is fundamental to leveraging the App Router effectively in your project structure.

Project Structure Overview

A well-organized project structure enhances readability, maintainability, and scalability. Here’s a recommended directory layout for a Next.js application using the App Router:

my-nextjs-app/
├── app/
│ ├── layout.js
│ ├── page.js
│ ├── dashboard/
│ │ ├── layout.js
│ │ ├── page.js
│ │ └── settings/
│ │ └── page.js
│ └── blog/
│ ├── layout.js
│ ├── page.js
│ └── [slug]/
│ └── page.js
├── components/
│ ├── Header.js
│ ├── Footer.js
│ └── ...
├── styles/
│ ├── globals.css
│ └── ...
├── public/
│ ├── images/
│ └── ...
├── lib/
│ ├── api.js
│ └── ...
├── hooks/
│ ├── useAuth.js
│ └── ...
├── tests/
│ ├── components/
│ └── ...
├── package.json
├── next.config.js
└── ...

Key Directories and Files

  • app/: Contains all the routes and layouts leveraging the App Router.
  • components/: Reusable UI components.
  • styles/: Global and component-specific styles.
  • public/: Static assets like images, fonts, etc.
  • lib/: Utility functions and libraries.
  • hooks/: Custom React hooks.
  • tests/: Test suites for various parts of the application.

Organizing Pages and Routes

With the App Router, Next.js encourages a file-based routing system, where the directory structure mirrors the URL structure. Here’s how to organize your pages and routes effectively:

Nested Routes and Layouts

Leverage nested routing to create hierarchical layouts. For example, a dashboard section might have its own layout separate from the main application layout.

app/
├── layout.js // Main application layout
├── page.js // Home page
├── dashboard/
│ ├── layout.js // Dashboard-specific layout
│ ├── page.js // Dashboard home
│ └── settings/
│ └── page.js // Dashboard settings

Dynamic Routes

Handle dynamic content by creating folders with bracket notation. For instance, blog posts can be accessed via /blog/[slug].

app/
└── blog/
└── [slug]/
└── page.js

In page.js:

export default function BlogPost({ params }) {
const { slug } = params;
// Fetch and render blog post based on slug
}

Catch-All Routes

For routes that need to capture multiple segments, use the [...param] syntax.

app/
└── docs/
└── [...slug]/
└── page.js

This setup can handle URLs like /docs/intro/getting-started.

Managing Components Effectively

Organizing components is vital for reusability and maintainability.

Atomic Design Principles

Adopt atomic design principles to categorize components:

  • Atoms: Basic building blocks like buttons, inputs.
  • Molecules: Combinations of atoms forming functional units.
  • Organisms: Complex UI sections composed of molecules and atoms.
  • Templates: Page-level structures.
  • Pages: Specific instances combining templates with data.

Directory structure:

components/
├── atoms/
│ ├── Button.js
│ └── Input.js
├── molecules/
│ ├── FormGroup.js
│ └── Card.js
├── organisms/
│ ├── Header.js
│ └── Footer.js
└── ...

Component Naming and File Structure

Use clear and consistent naming conventions. For example, Button.js for the component and Button.module.css for its styles.

// components/atoms/Button.js
import styles from './Button.module.css';

export default function Button({ children, onClick }) {
return (
<button className={styles.button} onClick={onClick}>
{children}
</button>
);
}

Styling Strategies

Choosing the right styling approach can impact your development workflow and application performance.

CSS Modules

Next.js supports CSS Modules out of the box, enabling scoped and modular CSS.

components/
└── atoms/
└── Button.module.css

Styled Components or CSS-in-JS

For dynamic styling and theming, consider libraries like Styled Components or Emotion.

// components/atoms/Button.js
import styled from 'styled-components';

const StyledButton = styled.button`
background-color: #0070f3;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
`;
export default function Button({ children, onClick }) {
return <StyledButton onClick={onClick}>{children}</StyledButton>;
}

Tailwind CSS

Tailwind offers utility-first CSS classes for rapid styling without leaving your HTML.

// components/atoms/Button.js
export default function Button({ children, onClick }) {
return (
<button
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
onClick={onClick}
>
{children}
</button>
);
}

Global Styles

Use the globals.css file for base styles and global configurations.

/* styles/globals.css */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

Import it in your root layout:

// app/layout.js
import '../styles/globals.css';

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

State Management Best Practices

Effective state management ensures your application remains predictable and easy to debug.

Local State with React Hooks

Use React’s built-in hooks like useState and useReducer for component-level state.

import { useState } from 'react';

export default function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

Global State with Context API

For sharing state across multiple components, leverage React’s Context API.

// context/AuthContext.js
import { createContext, useState } from 'react';

export const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);

return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}

Wrap your application with the provider:

// app/layout.js
import { AuthProvider } from '../context/AuthContext';

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}

State Management Libraries

For complex state needs, consider libraries like Redux, Zustand, or Recoil.

// Using Zustand
import create from 'zustand';

const useStore = create(set => ({
user: null,
setUser: (user) => set({ user }),
}));
export default useStore;

Optimizing Performance

Performance optimization ensures a smooth user experience and better SEO rankings.

Code Splitting and Lazy Loading

Next.js automatically splits code, but you can further optimize using dynamic imports.

import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Loading...</p>,
});

Image Optimization

Use Next.js’s Image component for optimized image loading.

import Image from 'next/image';

export default function Profile() {
return (
<Image
src="/images/profile.jpg"
alt="Profile Picture"
width={200}
height={200}
/>
);
}

Caching and CDN

Leverage caching strategies and Content Delivery Networks (CDNs) to serve static assets efficiently.

Server-Side Rendering (SSR) and Static Site Generation (SSG)

Choose between SSR and SSG based on your content’s dynamic nature to balance performance and freshness.

// pages/index.js
export async function getStaticProps() {
const data = await fetchData();
return {
props: { data },
revalidate: 60, // Revalidate every 60 seconds
};
}

Testing and Quality Assurance

Implementing testing ensures your application remains robust and free of regressions.

Unit Testing

Use frameworks like Jest and React Testing Library for unit tests.

// tests/components/Button.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '../../components/atoms/Button';

test('renders button and handles click', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click Me</Button>);

const button = screen.getByText('Click Me');
fireEvent.click(button);

expect(handleClick).toHaveBeenCalledTimes(1);
});

Integration Testing

Test how different parts of your application work together.javascript

// tests/pages/HomePage.test.js
import { render, screen } from '@testing-library/react';
import HomePage from '../../app/page';

test('renders home page with header and footer', () => {
render(<HomePage />);
expect(screen.getByText('Welcome')).toBeInTheDocument();
expect(screen.getByText('Footer')).toBeInTheDocument();
});

End-to-End (E2E) Testing

Use tools like Cypress or Playwright for E2E tests to simulate user interactions.

// cypress/integration/home.spec.js
describe('Home Page', () => {
it('loads successfully', () => {
cy.visit('/');
cy.contains('Welcome').should('be.visible');
});
});

Deployment Considerations

Deploying your Next.js application efficiently ensures minimal downtime and optimal performance.

Vercel

Vercel, created by the same team behind Next.js, offers seamless deployment with optimized performance.

Other Platforms

You can also deploy on platforms like Netlify, AWS, or DigitalOcean. Ensure proper configuration for SSR and API routes.

Environment Variables

Manage sensitive data using environment variables.

# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
SECRET_KEY=your-secret-key

Access them in your application:

const apiUrl = process.env.NEXT_PUBLIC_API_URL;

Continuous Integration and Deployment (CI/CD)

Implement CI/CD pipelines using GitHub Actions, GitLab CI, or other tools to automate testing and deployment.

# .github/workflows/ci.yml
name: CI

on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Dependencies
run: npm install
- name: Run Tests
run: npm test
- name: Build
run: npm run build

Conclusion

Structuring your Next.js application using the App Router effectively is pivotal for creating scalable, maintainable, and high-performance web applications. By adhering to the best practices outlined in this guide — from organizing your project structure and managing components to optimizing performance and implementing robust testing — you can harness the full potential of Next.js and deliver exceptional user experiences.

Embrace these practices to streamline your development workflow, facilitate team collaboration, and ensure your application stands the test of time in an ever-evolving web landscape.

Happy coding! If you found this guide helpful, feel free to share it with your fellow developers and leave your thoughts in the comments below.

--

--

Thiraphat Phutson
Thiraphat Phutson

Written by Thiraphat Phutson

I'm a software developer committed to improving the world with technology. I craft web apps, explore AI, and share tech insights.