Key takeaways
- Folder names define URL segments unless they are route groups or private folders.
- Dynamic, catch-all, and optional catch-all segments produce different params shapes.
- Layouts, loading states, and errors belong to route segments, not arbitrary URLs.
- Test concrete URLs and collisions before moving route folders.
The Next.js App Router turns folders and special files into a route tree. A page file makes a segment publicly reachable, a layout wraps descendant segments, and loading and error files define boundaries. Dynamic brackets add parameters, while parentheses and leading underscores organize code without becoming URL text.
The Next.js Dynamic Route Parser helps translate a route pattern into the params it can produce. It is a planning aid; the project build remains authoritative because Next.js also evaluates conflicting paths, special files, rewrites, configuration, and framework-version rules.
Begin with the URL, then map folders
For a target URL such as /shop/shoes/red-runner, write down which parts are fixed and which come from data. If shop and shoes represent stable product hierarchy, static folders make sense. If shoes is a category record and red-runner is a product record, app/shop/[category]/[product]/page.tsx expresses that contract.
A single dynamic segment uses [slug] and captures one path part. A catch-all uses [...slug] and captures one or more parts as an array. An optional catch-all uses [[...slug]] and also matches the parent path with no captured parts. Choose the narrowest pattern that represents the product; a broad catch-all can hide missing route design and complicate validation.
Route groups use parentheses, such as (marketing), and do not appear in the URL. They help share layouts or organize teams. Private folders beginning with an underscore are non-routable and useful for colocated components and helpers. Neither is a security boundary: sensitive logic still belongs on the server with real authorization.
Segment examples
app/blog/[slug]/page.tsx → /blog/hello-world
app/shop/[...parts]/page.tsx → /shop/shoes/red
app/docs/[[...parts]]/page.tsx → /docs and /docs/getting-started
app/(marketing)/about/page.tsx → /about
app/blog/_components/Card.tsx → no route
For [slug], params contains a string. For catch-all forms, the captured value is an array of strings; optional catch-all may be absent for the parent URL. Validate these values at the data boundary. A URL-shaped string is still untrusted input and may refer to no record.
A route-planning workflow
- List real URLs. Include the home page, detail pages, empty states, nested sections, and legacy redirects. Concrete examples expose ambiguity better than folder names alone.
- Mark stable and variable segments. Stable product concepts become ordinary folders. Database identifiers, usernames, slugs, dates, and locale codes may become dynamic segments.
- Choose the narrowest dynamic form. Prefer [slug] when exactly one segment is expected. Use catch-all only when hierarchy depth is genuinely variable.
- Place shared UI intentionally. A layout persists across descendants. Put it at the lowest segment that truly owns the navigation, data context, or visual shell.
- Design boundaries. Add loading UI around work that can suspend, errors where recovery is meaningful, and not-found handling where a parameter fails to resolve.
- Check collisions. Static paths should win conceptually over dynamic fallbacks, but duplicate public paths from different groups are invalid. Review the complete route tree.
- Generate representative params. Use generateStaticParams when prerendering known dynamic routes is valuable, but also decide what happens for an unknown value.
- Test navigation and direct loads. Client navigation, refresh, deep links, and deployment rewrites can exercise different parts of the system.
Layout ownership matters
Layouts preserve UI and state while navigating between descendants. This is useful for a dashboard sidebar or documentation navigation, but an overly high layout can retain state that should reset. A template has different remounting behavior and may be more appropriate when each navigation needs a fresh instance.
Multiple root layouts created through route groups can provide distinct shells, such as marketing and application areas. Navigating between different root layouts may require a full page load. Plan that boundary as a product decision, not merely a folder-cleanup trick.
Keep route-only helpers near the route in private folders, but avoid placing a second, unrelated application architecture inside every segment. Shared domain code can live in a clear project library; route-local code should be local because it is truly route-specific.
Dynamic params are input, not data
A slug does not prove a record exists, that the viewer may access it, or that it is canonical. Fetch the record, authorize the request on the server, and return notFound for absent content when appropriate. Redirect aliases to the canonical URL if the product supports renamed slugs.
Do not construct file paths, SQL fragments, or upstream URLs by concatenating an unchecked catch-all array. Decode and validate the expected shape, reject invalid values early, and use APIs that safely bind data. URL encoding also means the human-visible form and decoded parameter may differ.
For internationalized or multi-tenant applications, a top-level [locale] or [tenant] segment affects nearly every route. Validate it once in an owning layout or middleware-like boundary, then pass a typed result downward. Make the unsupported case explicit.
Common mistakes
- Using [[...slug]] when a small set of explicit routes would be clearer.
- Creating two route groups that resolve pages to the same public path.
- Assuming a private folder protects code or data from unauthorized users.
- Fetching the same record independently in layout, metadata, and page without a shared cached data function.
- Putting a global loading screen too high and replacing useful persistent navigation.
- Treating every unknown slug as a server error rather than a not-found result.
- Renaming folders without checking inbound links, sitemap entries, and canonical URLs.
- Testing only client-side links and missing failures on direct requests.
Route review checklist
- Every public URL has one unambiguous page owner.
- Dynamic values have documented string or array shapes.
- Catch-all routes are justified by variable depth.
- Layouts live at the lowest useful shared boundary.
- Loading, error, and not-found behavior are intentional.
- Authorization happens for every protected dynamic record.
- Canonical URLs and redirects cover renamed or duplicated paths.
- Direct loads, refreshes, and production rewrites are tested.
Frequently asked questions
Do route groups affect params or URLs?
No. Parenthesized group names are omitted from the URL. They organize routes and can establish layouts, but two groups cannot each create a page for the same resulting path.
When should I use a catch-all route?
Use it when the product genuinely accepts an unknown number of hierarchical segments, such as a documentation tree. If the depth is known, explicit nested dynamic folders create clearer params and better validation.
Should a missing dynamic record throw an error?
Usually it should produce a not-found response, while infrastructure failures should reach an error boundary. Separating absence from failure gives users and search engines a more accurate result.
Can I read params in a Client Component?
Next.js provides client navigation hooks, but prefer resolving and validating data in Server Components when the work does not require browser interaction. Pass the resulting serializable data to focused Client Components.
Further reading
A good route tree mirrors the product’s information architecture: explicit where the product is explicit, dynamic only where data is dynamic, and bounded by useful loading, error, and authorization behavior.
Try the related tool
Apply the guide to a focused example, then verify the result in your own project.
Open the related tool →