Next.js course Β· Module 1: Next.js Configuration
Directory and File Structure in Next.js (app/ vs pages/)
In this lesson9
In Quantum Metropolis, engineers design advanced transportation systems that can operate both on classic roads and on new quantum teleportation routes. Similarly, Next.js offers two parallel routing systems: the traditional Pages Router and the modern App Router. In this module, we will thoroughly analyze the differences between these two approaches and understand how to organize files and directories in our projects.
Two Routing Philosophies in Next.js
Next.js originally relied on file-based routing in the pages/ folder. In Next.js 13, a new routing system based on the app/ folder was introduced. This evolution resembles the transition from classic transportation systems to quantum teleportation in Quantum Metropolis - the new technology is more advanced, but both still coexist.
Pages Router: The Classic Approach
The Pages Router is the original routing system in Next.js. It is simple, well-documented, and still widely used. In this convention, every JavaScript or TypeScript file in the pages/ directory automatically becomes a route in the application.
1pages/
2βββ index.js # Route: /
3βββ about.js # Route: /about
4βββ contact.tsx # Route: /contact
5βββ products/
6β βββ index.js # Route: /products
7β βββ [id].js # Route: /products/:id (dynamic)
8βββ api/
9 βββ hello.js # API Endpoint: /api/helloIn this system:
- Files directly map to URL paths
- Dynamic path segments are marked with square brackets
[param] - A special
api/directory is used for handling API endpoints - Each file exports a React component or API handler function by default
App Router: The Modern Approach
The App Router, introduced in Next.js 13 and refined in version 15, represents a new approach based on React Server Components. It is like transitioning from traditional vehicles to quantum teleportation - a fundamental change in how we think about web applications.
1app/
2βββ layout.tsx # Main layout (required)
3βββ page.tsx # Route: /
4βββ about/
5β βββ page.tsx # Route: /about
6βββ blog/
7β βββ page.tsx # Route: /blog
8β βββ layout.tsx # Layout specific to /blog and its sub-routes
9β βββ [slug]/
10β βββ page.tsx # Route: /blog/:slug (dynamic)
11βββ api/
12 βββ users/
13 βββ route.ts # API Endpoint: /api/usersIn the App Router:
- Only directories define routes, and special files within those directories serve specific functions
- The
page.tsxfile renders the page component for a given route - The
layout.tsxfile defines a shared layout for all pages in the directory and its subdirectories - Dynamic segments still use square brackets:
[param] - API endpoints are created using
route.tsfiles
File Naming and Conventions in App Router
The App Router introduces "convention over configuration" with a series of special files that have specific roles. This is like specialized modules in Quantum Metropolis systems - each with a defined function in the overall architecture.
Basic Special Files:
page.tsx: Renders the route's user interface and makes the path publicly accessible.
1// app/dashboard/page.tsx 2export default function DashboardPage() { 3 return ( 4 <section> 5 <h1>Quantum Metropolis Control Panel</h1> 6 <p>Welcome to the command center!</p> 7 </section> 8 ); 9}layout.tsx: Defines a shared interface for a segment and its children.
1// app/dashboard/layout.tsx 2export default function DashboardLayout({ 3 children, 4}: { 5 children: React.ReactNode; 6}) { 7 return ( 8 <div className="dashboard-layout"> 9 <nav className="dashboard-sidebar"> 10 {/* Sidebar navigation */} 11 </nav> 12 <main className="dashboard-main">{children}</main> 13 </div> 14 ); 15}route.ts: Creates an API endpoint using the new request handling API.
1// app/api/status/route.ts 2import { NextResponse } from 'next/server'; 3 4export async function GET() { 5 return NextResponse.json({ 6 status: 'online', 7 systemsOperational: 15, 8 time: new Date().toISOString() 9 }); 10} 11 12export async function POST(request: Request) { 13 const data = await request.json(); 14 // Processing data... 15 return NextResponse.json({ success: true }); 16}loading.tsx: Creates a loading interface with React Suspense.
1// app/dashboard/loading.tsx 2export default function DashboardLoading() { 3 return ( 4 <div className="dashboard-loading"> 5 <div className="spinner"></div> 6 <p>Loading Metropolis system data...</p> 7 </div> 8 ); 9}error.tsx: Creates an error handling interface with React Error Boundary.
1'use client'; // Error handling components must be client components 2 3import { useEffect } from 'react'; 4 5export default function DashboardError({ 6 error, 7 reset, 8}: { 9 error: Error; 10 reset: () => void; 11}) { 12 useEffect(() => { 13 // Log error to monitoring system 14 console.error(error); 15 }, [error]); 16 17 return ( 18 <div className="error-container"> 19 <h2>An error occurred in the Metropolis systems!</h2> 20 <p>{error.message}</p> 21 <button onClick={reset}>Try again</button> 22 </div> 23 ); 24}not-found.tsx: Creates an interface for a not-found resource (404).
1// app/not-found.tsx 2export default function NotFound() { 3 return ( 4 <div className="not-found"> 5 <h2>Unknown Zone</h2> 6 <p>No Quantum Metropolis module was found with that identifier.</p> 7 <a href="/">Return to command center</a> 8 </div> 9 ); 10}
Advanced App Router Features
The App Router introduces several advanced organizational features that resemble the advanced transportation systems in Quantum Metropolis - they allow for more complex and flexible routes.
1. Route Groups
Route groups allow organizing files without affecting the URL structure. They are marked by enclosing the directory name in parentheses: (group-name).
1app/
2βββ (marketing)/ # Route group (does not affect URL path)
3β βββ about/
4β β βββ page.tsx # Route: /about
5β βββ contact/
6β βββ page.tsx # Route: /contact
7βββ (dashboard)/ # Another route group
8 βββ layout.tsx # Shared layout for dashboard routes
9 βββ page.tsx # Route: /
10 βββ stats/
11 βββ page.tsx # Route: /statsRoute groups are like districts in Quantum Metropolis - they help organize the city structure, but don't affect addresses.
2. Parallel Routes
Parallel routes allow simultaneous rendering of multiple pages in the same view, which is useful for complex layouts. They are marked by a slot name with the "@" sign: @slot-name.
1app/
2βββ layout.tsx # Main layout
3βββ page.tsx # Main page
4βββ dashboard/
5 βββ layout.tsx # Layout with multiple slots
6 β // layout.tsx defines slots {children, @stats, @notifications}
7 βββ page.tsx # Default content for the main slot
8 βββ @stats/
9 β βββ page.tsx # Content for the @stats slot
10 β βββ default.tsx # Required in Next.js 16 (e.g. returns null)
11 βββ @notifications/
12 βββ page.tsx # Content for the @notifications slot
13 βββ default.tsx # Required in Next.js 16 (e.g. returns null)In Next.js 16 every slot needs a default.tsx file (for example one that returns null or calls notFound()), otherwise next build fails.
1// app/dashboard/layout.tsx
2export default function DashboardLayout({
3 children,
4 stats,
5 notifications,
6}: {
7 children: React.ReactNode;
8 stats: React.ReactNode;
9 notifications: React.ReactNode;
10}) {
11 return (
12 <div className="dashboard-grid">
13 <main>{children}</main>
14 <aside className="stats-panel">{stats}</aside>
15 <section className="notifications-panel">{notifications}</section>
16 </div>
17 );
18}Parallel routes are like parallel dimensions in Quantum Metropolis - different realities accessible simultaneously.
3. Intercepting Routes
These allow "intercepting" a route and displaying it differently, which is ideal for modals that preserve navigation context. They are marked by the segment name with the sign "(.)": (.)name, (..)name or (...)name.
1app/
2βββ layout.tsx
3βββ page.tsx # Route: /
4βββ dashboard/
5 βββ layout.tsx
6 βββ page.tsx # Route: /dashboard
7 βββ projects/
8 βββ layout.tsx # Renders {children} and {modal}
9 βββ page.tsx # Route: /dashboard/projects
10 βββ [id]/
11 β βββ page.tsx # Route: /dashboard/projects/:id
12 βββ create/
13 β βββ page.tsx # Route: /dashboard/projects/create (full page)
14 βββ @modal/
15 βββ default.tsx # Returns null when the modal is closed
16 βββ (.)create/
17 βββ page.tsx # Modal over the list when opened via a linkWhen you follow a link from the list, Next.js shows the intercepted page in the @modal slot over the list; after a refresh or when you open the address directly, it shows the full create/page.tsx page.
Intercepting routes are like quantum shortcuts in Quantum Metropolis - they allow quick access to resources without changing context.
Code Organization in Next.js 16
Besides the routing structure, Next.js 16 suggests specific ways of organizing code. This is like planning districts in Quantum Metropolis - proper organization increases efficiency and readability.
Shared Components and Resources
Components used in multiple places should be organized consistently. Here is the recommended structure:
1src/
2βββ app/ # Application with App Router
3β βββ layout.tsx
4β βββ page.tsx
5βββ components/ # Shared components
6β βββ ui/ # Basic UI components
7β β βββ Button.tsx
8β β βββ Card.tsx
9β βββ features/ # Feature-specific components
10β βββ dashboard/
11β βββ auth/
12βββ lib/ # Shared functions and classes
13β βββ api/
14β βββ utils/
15βββ hooks/ # Custom React hooks
16β βββ useAuth.ts
17β βββ useLocalStorage.ts
18βββ styles/ # Global styles
19 βββ globals.cssCo-location vs. Centralization
Next.js 16 and the App Router support a "co-location" approach, where files related to a specific route are placed close together. This is like micro-districts in Quantum Metropolis - everything you need is nearby.
1app/
2βββ dashboard/
3 βββ page.tsx
4 βββ loading.tsx # Loading for this route
5 βββ error.tsx # Error handling for this route
6 βββ layout.tsx # Layout for this route
7 βββ actions.ts # Server Actions for this route
8 βββ components/ # Components specific to this route
9 β βββ StatusCard.tsx
10 β βββ DashboardNav.tsx
11 βββ styles/ # Styles specific to this route
12 βββ dashboard.module.cssComparing App Router and Pages Router
Now that we've explored both routing systems, let's compare them - just as Quantum Metropolis engineers might compare classic transport with quantum teleportation.
Rendering
Pages Router:
- Every page is pre-rendered to HTML by default (automatic static optimization), then hydrated in the browser
- SSR and SSG available through getServerSideProps and getStaticProps
- Relatively simple data models
App Router:
- Server Components by default
- Choice between Server and Client Components
- More advanced data flow model
- Streaming rendering
In the Pages Router, a page gets its data from a function exported next to the component, e.g. getStaticProps, which Next.js calls once, during the build:
1// pages/posts.js (Pages Router)
2export async function getStaticProps() {
3 const posts = await getPosts();
4 return { props: { posts } };
5}The getServerSideProps function has the same shape, but Next.js calls it on every request.
Loading State
Pages Router:
- Manual loading state management
- Often uses external libraries
App Router:
- Built-in loading.tsx components
- Integration with React Suspense
- Automatic loading indicators
Error Handling
Pages Router:
- Manual error management
- General component for 500 errors
App Router:
- error.tsx components with granular error handling
- Integration with React Error Boundary
- Ability to recover/retry after an error
Layouts
Pages Router:
- Manual layout creation with _app.js component
- One layout for the entire application
- Nested layouts require additional work
App Router:
- Built-in layout system through layout.tsx
- Ability to create nested layouts
- Layout sharing is simple and intuitive
When to Use Which Routing System?
Choosing between App Router and Pages Router resembles choosing between quantum teleportation and traditional transport in Quantum Metropolis - it depends on specific needs.
Use App Router when:
- You are creating a new project from scratch
- You want to use the latest React and Next.js features
- You need advanced layout and organization capabilities
- You care about better performance and streaming rendering
- You want to use React Server Components
Use Pages Router when:
- You have an existing project based on Pages Router
- You need a simpler mental model
- You use libraries that are not yet compatible with the App Router
- You want to use ready-made patterns and solutions
Migration from Pages Router to App Router
If you have an existing project using Pages Router, you can gradually migrate to App Router. This is like modernizing districts in Quantum Metropolis - you don't have to rebuild everything at once.
Next.js 16 allows both systems to coexist. You can start using app/ for new features while keeping existing pages in pages/.
1.
2βββ app/ # New features using App Router
3β βββ new-feature/
4β β βββ page.tsx # /new-feature
5β βββ layout.tsx
6βββ pages/ # Existing pages using Pages Router
7 βββ index.js # /
8 βββ about.js # /aboutImportant notes about migration:
- Routes in
app/take priority over routes inpages/, but the same URL cannot exist in both directories: Next.js reports an error during the build - Both structures use the same Next.js configuration
- You can gradually move pages from
pages/toapp/
Best Practices for File Organization
Regardless of the chosen routing system, it's worth following certain best practices:
Feature-oriented structure: Organize components and code around business features, not by technical types.
1src/ 2βββ features/ 3β βββ auth/ 4β β βββ components/ 5β β βββ hooks/ 6β β βββ utils/ 7β βββ dashboard/ 8β βββ components/ 9β βββ hooks/ 10β βββ utils/Modular approach: Treat each feature as an isolated module with a clearly defined API.
Consistent naming conventions: Establish conventions and stick to them throughout the project.
- Component files: PascalCase (Button.tsx)
- Hooks: camelCase with "use" prefix (useAuth.ts)
- Helper functions: camelCase (formatDate.ts)
Relative vs. absolute imports: Use import aliases (
@/) for better readability and easier file moving.Test co-location: Place tests close to the tested components.
1src/ 2βββ components/ 3β βββ Button.tsx 4β βββ Button.test.tsx
Summary
The directory and file structure in Next.js 16 is like the architecture of Quantum Metropolis - with proper planning, it can be both elegant and functional. The App Router introduces a more advanced but also more flexible application organization model, while the Pages Router offers a simpler and more direct approach.
The choice between them depends on the specific needs of the project, but for new projects, it is recommended to use the App Router to take advantage of the latest features and optimizations. Remember that good project organization is the key to easy maintenance and extensibility in the future.
In the next lesson, we will dive into the Next.js 16 component system, particularly the differences between Client Components and Server Components, which are a fundamental part of the App Router.
Code for this lesson: App.tsx
1// Props and State - Metropolis Quantum
2import React, { useState } from 'react';
3
4console.log("Props and State in React");
5console.log("Data management in Quantum City\n");
6
7// ==========================================
8// 1. PROPS - Passing Data
9// ==========================================
10console.log("=== 1. PROPS - Properties ===");
11
12interface UserCardProps {
13 name: string;
14 role: string;
15 level: number;
16 avatar: string;
17}
18
19const UserCard: React.FC<UserCardProps> = ({ name, role, level, avatar }) => {
20 return (
21 <div style={{
22 background: 'rgba(255, 255, 255, 0.1)',
23 padding: '20px',
24 borderRadius: '12px',
25 border: '2px solid #64ffda',
26 display: 'flex',
27 alignItems: 'center',
28 gap: '15px',
29 marginBottom: '15px'
30 }}>
31 <div style={{ fontSize: '3rem' }}>{avatar}</div>
32 <div>
33 <h3 style={{ color: '#64ffda', margin: '0 0 5px 0' }}>{name}</h3>
34 <p style={{ margin: '5px 0', color: '#b0bec5' }}>Role: {role}</p>
35 <p style={{ margin: '5px 0', color: '#ff9800' }}>Level: {level}</p>
36 </div>
37 </div>
38 );
39};
40
41// ==========================================
42// 2. STATE - Component State
43// ==========================================
44console.log("\n=== 2. STATE - useState Hook ===");
45
46function QuantumCounter() {
47 // useState returns [value, function to change value]
48 const [energy, setEnergy] = useState<number>(50);
49 const [isCharging, setIsCharging] = useState<boolean>(false);
50
51 const charge = () => {
52 if (energy < 100) {
53 setEnergy(energy + 10);
54 setIsCharging(true);
55 setTimeout(() => setIsCharging(false), 300);
56 console.log(`Charging energy: ${energy + 10}%`);
57 }
58 };
59
60 const discharge = () => {
61 if (energy > 0) {
62 setEnergy(energy - 10);
63 console.log(`Discharging energy: ${energy - 10}%`);
64 }
65 };
66
67 const reset = () => {
68 setEnergy(50);
69 console.log("Energy reset to 50%");
70 };
71
72 const getEnergyColor = () => {
73 if (energy > 70) return '#4caf50';
74 if (energy > 30) return '#ff9800';
75 return '#f44336';
76 };
77
78 return (
79 <div style={{
80 background: 'rgba(124, 77, 255, 0.2)',
81 padding: '20px',
82 borderRadius: '12px',
83 border: '2px solid #7c4dff'
84 }}>
85 <h3 style={{ color: '#7c4dff' }}>Quantum Energy Generator</h3>
86
87 <div style={{
88 background: 'rgba(0, 0, 0, 0.5)',
89 padding: '15px',
90 borderRadius: '8px',
91 marginBottom: '15px'
92 }}>
93 <div style={{
94 fontSize: '2rem',
95 textAlign: 'center',
96 color: getEnergyColor(),
97 marginBottom: '10px'
98 }}>
99 {energy}%
100 </div>
101 <div style={{
102 width: '100%',
103 height: '20px',
104 background: 'rgba(255, 255, 255, 0.1)',
105 borderRadius: '10px',
106 overflow: 'hidden'
107 }}>
108 <div style={{
109 width: `${energy}%`,
110 height: '100%',
111 background: `linear-gradient(90deg, ${getEnergyColor()}, #64ffda)`,
112 transition: 'all 0.3s ease',
113 transform: isCharging ? 'scale(1.05)' : 'scale(1)'
114 }}></div>
115 </div>
116 </div>
117
118 <div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
119 <button
120 onClick={charge}
121 disabled={energy >= 100}
122 style={{
123 background: energy >= 100 ? '#555' : 'linear-gradient(45deg, #4caf50, #8bc34a)',
124 color: 'white',
125 border: 'none',
126 padding: '10px 20px',
127 borderRadius: '6px',
128 cursor: energy >= 100 ? 'not-allowed' : 'pointer',
129 fontWeight: 'bold'
130 }}
131 >
132 Charge
133 </button>
134
135 <button
136 onClick={discharge}
137 disabled={energy <= 0}
138 style={{
139 background: energy <= 0 ? '#555' : 'linear-gradient(45deg, #f44336, #e91e63)',
140 color: 'white',
141 border: 'none',
142 padding: '10px 20px',
143 borderRadius: '6px',
144 cursor: energy <= 0 ? 'not-allowed' : 'pointer',
145 fontWeight: 'bold'
146 }}
147 >
148 Discharge
149 </button>
150
151 <button
152 onClick={reset}
153 style={{
154 background: 'linear-gradient(45deg, #607d8b, #90a4ae)',
155 color: 'white',
156 border: 'none',
157 padding: '10px 20px',
158 borderRadius: '6px',
159 cursor: 'pointer',
160 fontWeight: 'bold'
161 }}
162 >
163 Reset
164 </button>
165 </div>
166 </div>
167 );
168}
169
170// ==========================================
171// 3. Props + State Together
172// ==========================================
173console.log("\n=== 3. Props + State Combined ===");
174
175interface SystemStatusProps {
176 systemName: string;
177 initialStatus: 'online' | 'offline' | 'maintenance';
178}
179
180const SystemStatus: React.FC<SystemStatusProps> = ({ systemName, initialStatus }) => {
181 const [status, setStatus] = useState(initialStatus);
182 const [uptime, setUptime] = useState(0);
183
184 const toggleStatus = () => {
185 const statuses: Array<'online' | 'offline' | 'maintenance'> = ['online', 'offline', 'maintenance'];
186 const currentIndex = statuses.indexOf(status);
187 const nextStatus = statuses[(currentIndex + 1) % statuses.length];
188 setStatus(nextStatus);
189 setUptime(0);
190 console.log(`${systemName} status changed to: ${nextStatus}`);
191 };
192
193 const getStatusColor = () => {
194 switch(status) {
195 case 'online': return '#4caf50';
196 case 'offline': return '#f44336';
197 case 'maintenance': return '#ff9800';
198 }
199 };
200
201 return (
202 <div style={{
203 background: 'rgba(255, 255, 255, 0.1)',
204 padding: '15px',
205 borderRadius: '10px',
206 border: `2px solid ${getStatusColor()}`,
207 marginBottom: '15px'
208 }}>
209 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
210 <div>
211 <h4 style={{ margin: '0 0 10px 0', color: '#64ffda' }}>{systemName}</h4>
212 <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
213 <div style={{
214 width: '12px',
215 height: '12px',
216 borderRadius: '50%',
217 background: getStatusColor(),
218 boxShadow: `0 0 10px ${getStatusColor()}`
219 }}></div>
220 <span style={{ color: getStatusColor(), textTransform: 'uppercase', fontWeight: 'bold' }}>
221 {status}
222 </span>
223 </div>
224 </div>
225 <button
226 onClick={toggleStatus}
227 style={{
228 background: 'linear-gradient(45deg, #2196f3, #21cbf3)',
229 color: 'white',
230 border: 'none',
231 padding: '8px 16px',
232 borderRadius: '6px',
233 cursor: 'pointer',
234 fontWeight: 'bold'
235 }}
236 >
237 Toggle Status
238 </button>
239 </div>
240 </div>
241 );
242};
243
244// ==========================================
245// Main App
246// ==========================================
247function App() {
248 const citizens = [
249 { name: 'Alex Quantum', role: 'Chief Architect', level: 42, avatar: '' },
250 { name: 'Nova Star', role: 'Data Scientist', level: 38, avatar: '' },
251 { name: 'Cypher Neo', role: 'Security Expert', level: 45, avatar: '' }
252 ];
253
254 return (
255 <div style={{
256 padding: '20px',
257 background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%)',
258 minHeight: '100vh',
259 color: 'white'
260 }}>
261 <h1 style={{ textAlign: 'center', color: '#64ffda' }}>Metropolis Control Panel</h1>
262
263 <div style={{ marginTop: '30px' }}>
264 <h2 style={{ color: '#7c4dff' }}>Citizens (Props Demo)</h2>
265 {citizens.map((citizen, index) => (
266 <UserCard
267 key={index}
268 name={citizen.name}
269 role={citizen.role}
270 level={citizen.level}
271 avatar={citizen.avatar}
272 />
273 ))}
274 </div>
275
276 <div style={{ marginTop: '30px' }}>
277 <h2 style={{ color: '#7c4dff' }}>Energy System (State Demo)</h2>
278 <QuantumCounter />
279 </div>
280
281 <div style={{ marginTop: '30px' }}>
282 <h2 style={{ color: '#7c4dff' }}>Systems Status (Props + State)</h2>
283 <SystemStatus systemName="Quantum Core" initialStatus="online" />
284 <SystemStatus systemName="Neural Network" initialStatus="maintenance" />
285 <SystemStatus systemName="Transport Grid" initialStatus="offline" />
286 </div>
287
288 <div style={{
289 marginTop: '30px',
290 background: 'rgba(100, 255, 218, 0.1)',
291 padding: '20px',
292 borderRadius: '10px',
293 borderLeft: '4px solid #64ffda'
294 }}>
295 <h3>Props vs State:</h3>
296 <ul style={{ lineHeight: '1.8' }}>
297 <li><strong>Props</strong>: Passed from parent, read-only, like function parameters</li>
298 <li><strong>State</strong>: Internal component state, can be modified, re-renders component</li>
299 <li><strong>Props + State</strong>: Most commonly used together for dynamic UI</li>
300 </ul>
301 </div>
302 </div>
303 );
304}
305
306export default App;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What does 'declarative programming' mean in the context of React?
2. React components can be:
Hands-on tasks in the game
- Horizontal ordering
Arrange the path to a dynamic post page in Next.js
- Code editor
Create the About page component for the file pages/about.js (export default function About). The page should display an <h1> or <h2> heading with the company name and a <p> paragraph with information about the company.