Starting a project

Create a project, and know what every file the generator made is for.

02of 16
≈ 9Min read
7Parts

Creating one

the generator asks; these are the answers that matter
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.

the App Router layout
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

FileWhat it is
page.tsxMakes the folder a route, and renders it
layout.tsxA shell around this route and everything below it
loading.tsxShown while the page is being prepared
error.tsxCatches errors below it — a client component
not-found.tsxFor notFound() and unmatched routes
route.tsAn API endpoint instead of a page
template.tsxLike 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.

app/layout.tsx — required, and it owns html and body
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 useState here, and no onClick. That is the next lesson.
app/doors/page.tsx
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 start before 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.

next.config.js
/** @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;

Practice