
What is a design engineer, and why hire one for your website
A design engineer designs the website and builds it in code. One person, no handoff. What the role means, where it came from, and when hiring one makes sense.
There is no official recommended folder structure for the Next.js App Router. The docs lay out the options and leave the choice to you, which is honest and completely unhelpful when the repo is empty.

Written by Aleksi Huusko
Published at 7/30/2026

Most folder-structure arguments are about things you can rename in an afternoon.
A few decisions are load-bearing, and those are the ones worth thinking about before you commit. What follows is the layout behind a bilingual production site with a CMS behind it, so the parts I flag as painful are parts that have actually been painful.
apps/web/src/
app/
[lang]/
(site)/ ## marketing pages, with nav + footer
(funnel)/ ## bare landing pages, no chrome
layout.tsx ## the real root layout
api/
fonts/
components/
sections/ ## page-level blocks, one per CMS block type
elements/ ## small shared pieces
lib/ ## pure logic, no JSX
hooks/
proxy.tsFour top-level folders under src. If you want a fifth, you probably want a package instead.
Route groups are the folders wrapped in parentheses. They don't show up in the URL. They exist so two sets of pages can have different layouts.
The split worth making is chrome versus no chrome. Marketing pages want the navbar and footer. A landing page for a paid campaign or a lead magnet wants neither, because every link in a navbar is somewhere else for the visitor to go.
Putting those in (site) and (funnel) turns the difference into one file, instead of a conditional in a shared layout that grows a new branch every quarter.
Decide this early. Moving a route between groups changes which layout wraps it, and layouts are where your providers live.
If you localize with a dynamic segment, app/[lang]/layout.tsx is your real root layout. There is no app/layout.tsx at all, and the <html> and <body> tags sit one level down.
The consequence shows up in production rather than in development. A request to a path that doesn't exist has no locale segment to match, so it never reaches your layout, and the visitor gets the framework's built-in 404 instead of the one you designed.
Adding an app/layout.tsx does not fix this. You cannot have two root layouts, and adding one breaks the case that was already working. It's a genuine trade off of locale-prefixed routing, and the practical response is to make sure your own links never 404 rather than to spend an afternoon styling a page nobody should reach. Still worth knowing before you pick the pattern.
components/sections/ holds page-level blocks: full-width things that own their own vertical spacing, composed into a page by a page builder. One file per CMS block type, so a content editor adding an FAQ accordion maps to exactly one file.
components/elements/ holds the small things sections are built from. Buttons, the logo, the language switcher, the cookie notice.
The boundary works because it answers a question someone actually asks out loud: can a content editor drop this on a page by itself? Yes means section, no means element. Compare that to atoms and molecules and organisms, where the classification comes down to a judgment call nobody makes the same way twice.
Locale tables, dictionaries, SEO metadata builders, URL helpers. If a file needs JSX it's a component, if it needs `useState` it's a hook, and everything else lives in lib/.
The test I actually use is whether the thing could be unit tested without a renderer. That's what keeps lib/ from becoming the drawer where unclassifiable files go to die.
Two files is a perfectly good size for hooks/. You don't have to fill it.
Next.js 16 renamed middleware.ts to proxy.ts, and the exported function has to be named proxy as well. Follow a tutorial written before that and your redirects silently never run, which is a fun afternoon.
The part to be careful about is the matcher. It's a static string, and every top-level path that isn't a locale has to be excluded by hand. Miss one and it gets redirected into a locale prefix with no route behind it. Analytics ingestion proxied through your own domain is the classic casualty, because it's a top level path that looks like a page and isn't one.
Write the exclusion list once, then add to it every time you add a top-level route. It is the least clever file in the repo and it will happily break something you won't notice for a week.
Typed routes make href a checked type, so a link to a route you deleted becomes a build error. That's useful. It is also narrower than most people expect when they switch it on.
Dynamic segments widen to string. A route like /blog/[slug] accepts any string at all, because typed routes verify that the route exists, not that the slug resolves to real content. A link to a blog post you deleted last month still type-checks.
Anything built by string concatenation is invisible to it. The moment you assemble a path out of a locale variable and a slug you are holding a plain string again, and locale-prefixed routing does that constantly. External links and CMS-authored links get no help at all, which on a content-driven site is where broken links actually come from.
So it catches the refactors you did deliberately and misses the content that rotted on its own. Treat it as rename safety rather than link validation, and keep real redirects for the second problem.
A single site doesn't need one. The signal is a second consumer of your code.
It earns its place here because the CMS Studio is a separate app that shares types and queries with the site, and sharing is what a package is for. The test is whether something outside apps/web needs it. If nothing does, it's a folder.
One rule is worth enforcing from day one: a package should have an explicit list of what's importable rather than a barrel file that re-exports everything. Barrel files make it impossible to see what depends on what, which is the problem the monorepo was supposed to solve.
A folder per route for colocated components. I've tried it. Most components turn out to be used twice, and then they move anyway.
A types/ folder. Types belong next to whatever they describe, and types derived from a CMS schema belong in one generated file you never hand-edit.
Having both utils/ and helpers/. Pick one word and stick to it.
A constants/ folder. Constants live next to the code that reads them.
Get route groups and the localization shape right, because those are structural and reversing them means touching every layout. After that, pick any reasonable component split and stop thinking about it. That one is a rename.
The signal that a structure is working is that a new file has an obvious home. If you have to stop and think about where something goes, the boundary is probably in the wrong place, and adding another level of nesting won't rescue it.

A design engineer designs the website and builds it in code. One person, no handoff. What the role means, where it came from, and when hiring one makes sense.

What a B2B marketing site costs in Europe in 2026, by provider type, with sources that publish their prices.

Which platform fits your B2B marketing site in 2026? A decision tree from a design engineer who ships both Webflow and Next.js, with real running costs.