Structuring a large app
Organize a growing Next.js app by feature: folders that make no routes, a server-only data module per feature, shared UI, @/ imports and ESLint rules.
- 01Choose a folder layoutSort a growing app's files by kind, by feature or beside the routes, knowing that only page and route files make addresses.
- 02Keep each feature's data on the serverGive each feature one server-only way in for its data, keep small helpers apart from it, and spot a barrel file that mixes server and client code.
- 03Share UI and write rules downKeep shared components plain, put "use client" on the leaves, import with @/, and let ESLint check which folder may import which.
One tea shop, two ways to sort its files
The tea shop has grown. It has teas, reviews, a cart, accounts, and a team for each. Next.js decides what the files in app do, and leaves the rest of your files to you (Project structure showed where each kind goes). In a small app, any order works. In a large one, where a file lives decides how many folders you open for one change, and whether two teams end up changing the same files. Here are the shop's teas and reviews, sorted the way many apps start, by kind of file:
app/
reviews/
page.tsx
teas/
page.tsx
layout.tsx
components/
add-to-cart.tsx
card.tsx
review-list.tsx
tea-card.tsx
lib/
format.ts
reviews.ts
teas.tscomponents holds every component and lib every other module. The teas are four files: components/tea-card.tsx, components/add-to-cart.tsx, lib/teas.ts and lib/format.ts. They sit in two folders, between the reviews' files. With ten features, each folder would hold dozens of files from all of them. Now the same files, sorted by feature: a part of the shop that a visitor would name, like the teas or the reviews.
app/
reviews/
page.tsx
teas/
page.tsx
layout.tsx
features/
reviews/
dal.ts
review-list.tsx
teas/
add-to-cart.tsx
dal.ts
format.ts
tea-card.tsx
ui/
card.tsxfeatures/teas holds everything about teas: the card, the button, the module that reads the teas, and a price helper. features/reviews does the same for reviews. ui/card.tsx is the one component both features use, so it sits below them, in a folder for shared UI. And app holds only routes. This version is a real Next.js 16.4.3 project, built with next build and run with next start; what it sent back was recorded. Click a file in the tree to read it.
app/teas/page.tsximport TeaCard from "@/features/teas/tea-card";import { getTeas } from "@/features/teas/dal"; export default async function TeasPage() { const teas = await getTeas(); return ( <main> <h1>Teas</h1> {teas.map((tea) => ( <TeaCard key={tea.id} tea={tea} /> ))} </main> );}The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
In app/teas/page.tsx, line 1 imports TeaCard from @/features/teas/tea-card, and line 2 imports getTeas from @/features/teas/dal. Line 5 waits for the teas, and lines 9 to 11 draw a card for each. The page is short: it puts the feature's parts together, and the feature does the work. The Browser tab shows the two teas with their buttons. The Terminal tab lists the routes: ○ /_not-found, ○ /reviews and ○ /teas, all prerendered as static content. In the Network tab, /features/teas/tea-card answered 404: a folder outside app is never part of an address.
components and lib. Sorted by feature, they sit together in features/teas. Both builds list the same routes, because only the files in app make them.A small kitchen can keep all its knives in one drawer and all its bowls on one shelf. A busy restaurant kitchen sorts by station instead: the salad station has its own knife, bowl and dressing within reach, so the cook never crosses the room. The plates that every station uses sit on one shared shelf. The guests only see the menu.
- In real life: A drawer with every knife in the kitchen
- In Next.js:
components/: every component in the app - In real life: The salad station, with its own knife, bowl and dressing
- In Next.js:
features/teas/: the teas' card, button, data module and helper - In real life: The plates every station uses, on one shared shelf
- In Next.js:
ui/: shared components, likeCard - In real life: The menu the guests read
- In Next.js:
app/: the routes visitors open
Next.js gives addresses only to the routing files in app, like page.tsx and route.ts; where the rest of your code lives is your choice. For a growing app, keep app for routes, group the other files by feature, give each feature one server-only module for its data, keep shared UI plain, and write the rules down where a tool can check them.
By kind of file, by feature, or beside the routes
Next.js's docs call the framework unopinionated about how you organize your project, and describe a few common ways. Three of them, for the same shop. Each row is one layout: where the teas' files go, and what that gives you.
| Layout | Where the teas' files are | What it gives you |
|---|---|---|
| By kind of file | components/tea-card.tsx, components/add-to-cart.tsx, lib/teas.ts, lib/format.ts: one folder for each kind of file. | It's quick to start. As the shop grows, components and lib fill with files from every part of it, and one change to teas touches both. |
| By feature | features/teas/: the card, the button, the data module and the helper, side by side. Shared parts go in ui/. | A change to teas stays in one folder, and a team can own one. Code that two features need goes down into ui/. |
| Beside the routes | app/teas/: next to the page that uses them. Shared parts go in a private folder, app/_ui/. | The route and its parts live together. When two routes need the same code, it has to move somewhere both can reach. |
The third layout keeps each route's files in its own folder in app, next to its page.tsx. That's called colocation, and its tree looks like this:
app/
_ui/
card.tsx
reviews/
dal.ts
page.tsx
review-list.tsx
teas/
add-to-cart.tsx
dal.ts
format.ts
page.tsx
tea-card.tsx
layout.tsxEach layout below is the same shop, built and run on its own. Before you click, guess: does any of them change the routes the build lists?
app/teas/page.tsximport TeaCard from "@/features/teas/tea-card";import { getTeas } from "@/features/teas/dal"; export default async function TeasPage() { const teas = await getTeas(); return ( <main> <h1>Teas</h1> {teas.map((tea) => ( <TeaCard key={tea.id} tea={tea} /> ))} </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (4/4) Finalizing page optimization ... Route (app)┌ ○ /_not-found├ ○ /reviews└ ○ /teas ○ (Static) prerendered as static contentapp holds only routes. Each feature's files live in features/, and the shared Card in ui/. The build lists ○ /reviews and ○ /teas, and /features/teas/tea-card answered 404.
None did. All three builds list ○ /_not-found, ○ /reviews and ○ /teas, and the pages show the same teas and reviews. In the colocated layout, app/teas/tea-card.tsx sits right beside page.tsx, and /teas/tea-card still answered 404, like /teas/dal and /_ui/card. Next.js only gives an address to a folder with a page.tsx or a route.ts in it, and a private folder like _ui never gets one, whatever it holds (Route groups and private folders).
A file without an address can still reach the browser. When a Client Component imports it, its code is sent with the component, and whatever a page renders is sent in its HTML and its RSC payload, the description of the rendered Server Components that goes with it. Keep secrets in server-only modules and server environment variables (Server-only code and environment variables).
So pick the layout for the people who read the code, not for Next.js. Small apps do well by kind of file. When components and lib fill with files from every part of the shop, sort by feature. The layouts also mix: the docs describe keeping code that the whole app shares at the top and splitting the rest by route, and route groups, folders in round brackets like (shop), sort the routes themselves by section or by team without changing an address.
A data access module for each feature
Every feature reads data somewhere. Give each one a single server-only way in, here features/teas/dal.ts: the teas' data access layer, or DAL, the name Next.js's data security guide gives the code that every read of your data goes through. Here it's one file; in a bigger feature it can be several modules behind one entry file, each starting with import "server-only". Authorization and the data access layer built one for orders, with a check of who is asking in every function. Teas are public, so this one only reads and picks fields:
import "server-only"; // A stand-in for the teas table. `cost` is what the shop pays: staff only.const rows = [ { id: "green", name: "Green tea", price: 4, cost: 1.2 }, { id: "mint", name: "Mint tea", price: 3, cost: 0.9 },]; export type Tea = { id: string; name: string; price: number }; export async function getTeas(): Promise<Tea[]> { return rows.map(({ id, name, price }) => ({ id, name, price }));} - Line 1,
import "server-only", makes the build stop if a Client Component's code imports this file (Server-only code showed the error). - Lines 4 to 7 stand in for the teas table. Each row has a
cost, what the shop pays for the tea, which only staff may see. - Line 9 names the shape the rest of the app gets:
Tea, with an id, a name and a price. - Line 12 returns those three fields and leaves
costbehind. An object made only to carry data out of a layer like this is a DTO, a data transfer object.
With one way in for each feature, the questions "where are the teas read?" and "which fields can a page get?" each have one answer. features/reviews/dal.ts does the same for reviews, and leaves each reviewer's email behind. Not every file in the feature belongs in it, though. Each row is one piece of the teas feature, and the file it goes in:
| The code | Where it goes | Why |
|---|---|---|
| Reading the teas table, or calling a service with a key | features/teas/dal.ts, which starts with import "server-only". | Every read goes through one file that only runs on the server and returns only the fields pages need. |
| Formatting a price | features/teas/format.ts, with no server-only line. | It holds no secrets, and the Add to cart button needs it in the browser. |
| The card that shows a tea | features/teas/tea-card.tsx, a Server Component. | It renders on the server, so its code stays there. |
| The Add to cart button | features/teas/add-to-cart.tsx, with "use client" at the top. | It keeps state and handles clicks, so its code goes to the browser. |
The Add to cart button shows the price, so it needs formatPrice. Here are three versions of the teas feature. Before you click, guess which ones build.
app/teas/page.tsximport TeaCard from "@/features/teas/tea-card";import { getTeas } from "@/features/teas/dal"; export default async function TeasPage() { const teas = await getTeas(); return ( <main> <h1>Teas</h1> {teas.map((tea) => ( <TeaCard key={tea.id} tea={tea} /> ))} </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (4/4) Finalizing page optimization ... Route (app)┌ ○ /_not-found├ ○ /reviews└ ○ /teas ○ (Static) prerendered as static contentLine 2 of the teas page imports getTeas from the data module. The page is a Server Component, so the module stays on the server, and the build lists ○ /teas.
What the build did in each version:
- The page imports it: the teas page is a Server Component, so
dal.tsstays on the server. This is the overview's shop. - The button imports a helper from it: someone put
formatPriceindal.ts, and the button imported it from there. The build stopped: 'server-only' cannot be imported from a Client Component module. (The first of its two messages mentions the Pages Router, which this app doesn't use.) The import trace under it starts with./features/teas/dal.ts [Client Component Browser]and./features/teas/add-to-cart.tsx [Client Component Browser]: a Client Component's imports go to the browser with it, the whole module, not only the function it uses. - The button imports through a barrel: a barrel file is a file that only re-exports other files, so the rest of the app can import a feature from one place. This one,
features/teas/index.ts, re-exports the whole feature, and line 2 re-exportsdal.ts. The button imported onlyformatPrice, from@/features/teas, and the build stopped with the same error. This time the trace runs through./features/teas/index.ts [Client Component Browser].
export { default as AddToCart } from "./add-to-cart";export { getTeas, type Tea } from "./dal";export { formatPrice } from "./format";export { default as TeaCard } from "./tea-card"; Line 1 re-exports the button, a Client Component, and line 2 the data module, which may only run on the server. When the button imports from this barrel, the compiler follows the barrel's imports to build the client's module graph, the list of modules the browser code depends on, and line 2 puts dal.ts in it. The server-only check looks at that graph, so the build stopped, although the button never uses getTeas. That's a separate question from how many bytes ship: a bundler may leave out exports nobody uses, but only when it can tell that dropping them changes nothing. Two fixes work: a client file imports the file it needs (./format), or a feature keeps no server-only module in a barrel that client code uses. The overview's shop has no barrel at all. Barrels can cost bytes too: Bundle size recorded a barrel of the shop's own files bringing a whole Markdown library to a page that only needed a button.
dal.ts and tea-card.tsx run only on the server, and the card renders the button (the arrow). add-to-cart.tsx starts with "use client", so its code and everything it imports, like format.ts, also go to the browser. If it imported dal.ts, import "server-only" would stop the build.Imports that survive a move: @/
Every import in the overview's pages starts with @/. That's a path alias: a short name for a folder, set in tsconfig.json. create-next-app writes this one, and every recorded project here has it:
{ "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./*"] } }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts" ], "exclude": ["node_modules"]} Lines 21 to 23 map @/* to ./*, the project's top folder. So @/features/teas/dal means the file features/teas/dal.ts, from any folder. Next.js reads the setting too, which is why the recorded builds found every file. Without it, app/teas/page.tsx would import ../../features/teas/dal. Move the page into a route group, to app/(shop)/teas/page.tsx, and that path breaks; the alias doesn't change.
A rule many teams follow, and the recorded files do: ./ for files in the same folder, like ./add-to-cart inside features/teas, and @/ for everything else. Then a feature can move as a whole without breaking its own imports, and an import line says where a file lives.
Rules a team can check
A layout only works while everyone follows it. Rules written in a README get forgotten; rules a tool checks don't. The shop's most important rule is about direction: app may import features, features may import ui, and ui imports neither, so a shared component never depends on one feature. ESLint, the linter create-next-app can set up, has a rule for this in its core, no-restricted-imports:
import { defineConfig, globalIgnores } from "eslint/config";import nextVitals from "eslint-config-next/core-web-vitals";import nextTs from "eslint-config-next/typescript"; const eslintConfig = defineConfig([ ...nextVitals, ...nextTs, { // Shared UI knows nothing about features or routes. files: ["ui/**"], rules: { "no-restricted-imports": ["error", { patterns: [{ group: ["@/features/*", "@/app/*"], message: "ui/ is shared by every feature: pass data in as props.", }], }], }, }, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: ".next/**", "out/**", "build/**", "next-env.d.ts", ]),]); export default eslintConfig; Lines 1 to 7, and the ignores after the block, are the config create-next-app writes when you pick ESLint. Lines 8 to 19 add a block. Line 10 says it's for files in ui/. Line 12 turns on no-restricted-imports as an error. Line 14 lists the imports it refuses: any path under @/features/ or @/app/. Line 15 is the message the report shows. Now someone adds a price tag to ui that formats the price with the teas' helper:
ui/price-tag.tsximport { formatPrice } from "@/features/teas/format"; export default function PriceTag({ price }: { price: number }) { return <span className="ui-price">{formatPrice(price)}</span>;}$ npx eslint ./my-app/ui/price-tag.tsx 1:1 error '@/features/teas/format' import is restricted from being used by a pattern. ui/ is shared by every feature: pass data in as props no-restricted-imports ✖ 1 problem (1 error, 0 warnings) $ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (4/4) Finalizing page optimization ... Route (app)┌ ○ /_not-found├ ○ /reviews└ ○ /teas ○ (Static) prerendered as static contentLine 1 of ui/price-tag.tsx imports formatPrice from @/features/teas/format. The Terminal tab starts with $ npx eslint ., which the recorder ran before the build: '@/features/teas/format' import is restricted from being used by a pattern. ui/ is shared by every feature: pass data in as props, on line 1, column 1 of /my-app/ui/price-tag.tsx (/my-app is the project's folder), then ✖ 1 problem (1 error, 0 warnings). ESLint ended with exit code 1. Then $ next build built every route anyway: since Next.js 16, next build doesn't run ESLint (Accessibility recorded the same). Run npx eslint . yourself, in the checks that run on every change, beside your tests (Testing Next.js apps).
The fix follows the message. PriceTag takes the text to show, and the tea card, inside the teas feature, formats the price:
ui/price-tag.tsxexport default function PriceTag({ text }: { text: string }) { return <span className="ui-price">{text}</span>;}$ npx eslint . $ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (4/4) Finalizing page optimization ... Route (app)┌ ○ /_not-found├ ○ /reviews└ ○ /teas ○ (Static) prerendered as static contentNow ui/price-tag.tsx imports nothing, and features/teas/tea-card.tsx passes text={formatPrice(tea.price)} on line 10. ESLint printed nothing and ended with exit code 0, and the page is unchanged: $4, then Add for $4.
app imports features, and features import ui. The ESLint rule in eslint.config.mjs reports any file in ui that imports from @/features or @/app.Each row is one rule the shop keeps, and what checks it:
| The rule | What checks it | What happens when it's broken |
|---|---|---|
| Shared UI never imports a feature. | ESLint's no-restricted-imports rule, for files in ui/. | npx eslint . reports the import and exits with code 1. |
A data module starts with import "server-only". | Next.js, while it builds. | next build stops if a Client Component's code imports the module. |
Imports use @/, not long ../../ paths. | Code review. A pattern in the same ESLint rule can forbid them too. | Files can move without breaking their neighbours' imports. |
| Each feature has an owner. | A CODEOWNERS file, on code hosts that support one, like GitHub. | Changes to a feature's folder ask its team for a review. |
Core ESLint's rule is enough for a few folders. For many layers, there are third-party plugins, such as eslint-plugin-boundaries, which let you name each kind of folder and list which may import which. None of the recorded examples uses one, so check its docs before you rely on it. Whatever the tool, the rules belong in the repository, next to the code they protect, so a new teammate learns them from the first report.
The tea shop's folders, sorted
Here is the whole shop by feature, as recorded in the overview. Pages and layouts stay in app; each feature has its components, its data module and its helpers; ui holds what both features use. The shop keeps adding code. Sort each new file into the folder it belongs in:
- The page at /teas
- The root layout with the shop's links
- The route.ts that answers /api/reviews
- The function that reads the teas table
- The Add to cart button
- The function that reads the reviews table
- The list of a tea's reviews
- A Card frame that teas and reviews both use
- A Button with the shop's colors, used everywhere
Sort each file into app, a feature's folder, or the shared ui folder.
One way to decide: if visitors open it at an address, it's in app. If only one feature uses it, it lives in that feature. If two features use it and it knows nothing about either, it's shared UI. Code that knows about one feature but is needed by another usually means the second feature should get the data from a page, as props.
Common mix-ups
- "Next.js wants feature folders." No. The three layouts built with the same routes. Next.js only reads the routing files in
app; the rest is for the people who read the code. - "Every file in
appis a page." No. Only apage.tsxor aroute.tsmakes an address./teas/tea-cardanswered 404, withtea-card.tsxright beside the page. - "A file with no address is private." No. A Client Component that imports it sends its code to the browser, and whatever a page renders is in its HTML and RSC payload.
- "A helper with no secrets can live in the data module." Not if a Client Component needs it. Importing one function brings the whole module, and
server-onlystops the build. Keep helpers in their own files. - "A barrel file is only a shortcut." No. Importing anything from it puts everything it re-exports in the importer's module graph: a server-only module in a client file's barrel stops the build, and an unused library can add bytes when the bundler can't leave it out.
- "Shared UI components need
"use client"." No. A component without state or event handlers works in Server and Client Components. The directive only sent the Card's code to the browser. - "
next buildchecks my lint rules." No. Since Next.js 16 it doesn't run ESLint; the recorded build passed right after ESLint reported an error.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
This is the shop sorted beside its routes. Of /teas, /reviews and /teas/tea-card, which address answers 404?
app/
_ui/
card.tsx
reviews/
dal.ts
page.tsx
review-list.tsx
teas/
add-to-cart.tsx
dal.ts
format.ts
page.tsx
tea-card.tsx
layout.tsx/teas/tea-card. Only page.tsx and route.ts make addresses, so app/teas/page.tsx is /teas and app/reviews/page.tsx is /reviews. The recording of this tree answered /teas/tea-card with 404.
Which of the three layouts is this: by kind of file, by feature, or beside the routes?
app/
reviews/
page.tsx
teas/
page.tsx
layout.tsx
components/
add-to-cart.tsx
card.tsx
review-list.tsx
tea-card.tsx
lib/
format.ts
reviews.ts
teas.tsBy kind of file. components holds every component and lib every other module, whatever part of the shop it belongs to. The teas' four files are spread across both.
The shop is sorted by feature. Write line 2 of app/reviews/page.tsx: the import of getReviews, with the @/ alias.
import ReviewList from "@/features/reviews/review-list";
// Import getReviews from the reviews feature's data module.
export default async function ReviewsPage() {
const reviews = await getReviews();
return (
<main>
<h1>Reviews</h1>
<ReviewList reviews={reviews} />
</main>
);
}
import ReviewList from "@/features/reviews/review-list";
import { getReviews } from "@/features/reviews/dal";
export default async function ReviewsPage() {
const reviews = await getReviews();
return (
<main>
<h1>Reviews</h1>
<ReviewList reviews={reviews} />
</main>
);
}
import { getReviews } from "@/features/reviews/dal";. @/ is the project's top folder, so the path works from app/reviews. It's line 2 of the recorded page, which shows Asha's and Ben's reviews.
This is ui/price-tag.tsx, linted with the shop's eslint.config.mjs. Which rule reports line 1?
import { formatPrice } from "@/features/teas/format";
export default function PriceTag({ price }: { price: number }) {
return <span className="ui-price">{formatPrice(price)}</span>;
}
no-restricted-imports. Its block applies to ui/**, and @/features/teas/format matches the pattern @/features/*. The recorded report ends its line with no-restricted-imports, and ESLint exits with code 1.
The build stops with 'server-only' cannot be imported from a Client Component module, because the Add to cart button imports formatPrice from this features/teas/dal.ts. Where should formatPrice go?
import "server-only";
// A stand-in for the teas table. `cost` is what the shop pays: staff only.
const rows = [
{ id: "green", name: "Green tea", price: 4, cost: 1.2 },
{ id: "mint", name: "Mint tea", price: 3, cost: 0.9 },
];
export type Tea = { id: string; name: string; price: number };
export function formatPrice(price: number) {
return `$${price}`;
}
export async function getTeas(): Promise<Tea[]> {
return rows.map(({ id, name, price }) => ({ id, name, price }));
}
export function formatPrice(price: number) {
return `$${price}`;
}
Into features/teas/format.ts, a file of its own with no server-only line, and the button imports it from ./format. The data module stays server-only. That's the overview's shop, which builds and lists ○ /teas.
The team adds this features/teas/index.ts, and the Add to cart button imports formatPrice from @/features/teas. Does next build pass?
export { default as AddToCart } from "./add-to-cart";
export { getTeas, type Tea } from "./dal";
export { formatPrice } from "./format";
export { default as TeaCard } from "./tea-card";
"use client";
import { useState } from "react";
import { formatPrice } from "./format";
export default function AddToCart({ price }: { price: number }) {
const [added, setAdded] = useState(false);
return (
<button type="button" onClick={() => setAdded(true)}>
{added ? "Added" : `Add for ${formatPrice(price)}`}
</button>
);
}
No, the build stops. Importing from the barrel puts all of its re-exports in the button's module graph, and line 2 re-exports dal.ts, which starts with import "server-only". The recorded trace runs through ./features/teas/index.ts [Client Component Browser]. Importing ./format directly, as here, builds.
Check your understanding
For each question, ask which files make addresses, and where each module's code runs.
Question 1 of 7In the shop sorted beside its routes,
app/teas/tea-card.tsxsits next to the teas page. What does/teas/tea-cardanswer?Choose an answer to see the explanation.
Question 2 of 7The shop was built three times: sorted by kind of file, by feature, and beside the routes. Which routes did
next buildlist?Choose an answer to see the explanation.
Question 3 of 7Why does
features/teas/dal.tsstart withimport "server-only"?Choose an answer to see the explanation.
Question 4 of 7This version of the Add to cart button imports
formatPricefrom./dal, which starts withimport "server-only". What doesnext builddo?Read the code, then predictTSX"use client"; import { useState } from "react"; import { formatPrice } from "./dal"; export default function AddToCart({ price }: { price: number }) { const [added, setAdded] = useState(false); return ( <button type="button" onClick={() => setAdded(true)}> {added ? "Added" : `Add for ${formatPrice(price)}`} </button> ); }Choose an answer to see the explanation.
Question 5 of 7Where does
"use client"belong in a sharedui/folder?Choose an answer to see the explanation.
Question 6 of 7In
import TeaCard from "@/features/teas/tea-card", what does@/mean?Choose an answer to see the explanation.
Question 7 of 7
npx eslint .reportedno-restricted-importsinui/price-tag.tsx, yetnext buildpassed. Why didn't the build stop?Choose an answer to see the explanation.
Key takeaways
- Next.js gives addresses only to routing files in
app. Sorting the rest by kind of file, by feature or beside the routes changed no route in the recordings. - For a growing app, keep
appfor routes and group the other files by feature, with shared UI in a folder of its own. - Give each feature one server-only way in for its data, a module or a few behind one entry, that returns only the fields pages need. Keep helpers that the browser needs in their own files.
- A barrel puts everything it re-exports in the module graph of whoever imports it: a server-only module behind it stops the build of any client file that uses it, even if nothing ships.
- Keep shared components plain, and put
"use client"on the small leaves that need the browser. - Import with
@/across folders, and let ESLint check which folder may import which. Run it yourself:next builddoesn't.
Remember the one-liner.
Routes in app, everything else by feature, data behind server-only, shared UI plain, and the rules in ESLint.
Coming next: Next.js as a backend for your frontend. The shop's Next.js app stands in front of other teams' services: Route Handlers as a public API, rewrites, and calling several services at once.