Creating one
npx create-next-app@latest doors
# TypeScript? yes
# ESLint? yes
# Tailwind CSS? your call
# src/ directory? yes — keeps the root clean
# App Router? YES — the Pages Router is the old one
# Turbopack for dev? yes
cd doors
npm run dev # http://localhost:3000
What is in there
Only files with the reserved names become routes. doors/DoorCard.tsx is not a page — you can keep components next to the route that uses them, which is one of the quiet benefits of the App Router.
src/
app/
layout.tsx ← the shell every page is rendered inside (required)
page.tsx ← "/"
globals.css
doors/
page.tsx ← "/doors"
[id]/
page.tsx ← "/doors/12"
components/
lib/
public/ ← served from the root: /logo.svg
next.config.js
package.json
The reserved file names
| File | What it is |
|---|---|
page.tsx | Makes the folder a route, and renders it |
layout.tsx | A shell around this route and everything below it |
loading.tsx | Shown while the page is being prepared |
error.tsx | Catches errors below it — a client component |
not-found.tsx | For notFound() and unmatched routes |
route.ts | An API endpoint instead of a page |
template.tsx | Like a layout, but remounted on every navigation |
page and route cannot share a folder
A folder is either a page or an endpoint. app/doors/page.tsx and app/doors/route.ts together is a build error.
The root layout
This is the only place <html> and <body> appear. There is no index.html in the project — the framework builds the document.
import "./globals.css";
export const metadata = {
title: "Doors",
description: "Made to order, in eight weeks.",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
A page
- The default export is the page. The name of the function is only for your stack traces.
- It is a server component unless it says otherwise — so it may be
async, and it ships no JavaScript to the browser. - No
useStatehere, and noonClick. That is the next lesson.
export default async function DoorsPage() {
const doors = await getDoors(); // it runs on the server: await is fine
return (
<main>
<h1>Doors</h1>
<ul>{doors.map((d) => <li key={d.id}>{d.name}</li>)}</ul>
</main>
);
}
The dev server, and its lies
- The first load of a route in development compiles it on demand: slow once, fast afterwards. It is not what production feels like.
- Always check
npm run build && npm startbefore drawing conclusions about speed. - The build output prints a table of every route and whether it is static or dynamic. Read it — it is the fastest way to notice a page that went dynamic by accident.
Configuration, and keeping it small
Most projects need three lines of it. A long next.config.js is usually a sign of fighting the framework rather than using it.
/** @type {import("next").NextConfig} */
const nextConfig = {
images: { remotePatterns: [{ protocol: "https", hostname: "cdn.domvybe.com" }] },
async redirects() {
return [{ source: "/old-doors", destination: "/doors", permanent: true }];
},
};
export default nextConfig;