This is a feature-based architecture with an explicit service layer for Next.js, React, and TypeScript projects. Here's how it works and why I used it.
This website looks like a personal site from the outside, a homepage, a few project pages, some writing, and a contact form.
But a website stops being “just a website” the moment it starts doing real work. Content comes from a CMS, a form sends email, bot protection checks requests, metadata has to be correct, webhooks need a stable endpoint, secrets need to stay on the server, tests need to prove the important paths still work.
That is why I did not structure this site as a pile of pages.
I used a feature-based architecture with an explicit service layer. It is not a branded pattern I am trying to sell. It is a practical shape for a Next.js, React, and TypeScript project that has to stay understandable after the first clean demo.
The high-level structure is simple:
app/ routing and page composition
features/ product behavior grouped by feature
services/ external systems behind typed wrappers
lib/ pure shared utilities
hooks/ shared client hooks when they are truly shared
env.ts one place for runtime configuration
e2e/ browser-level testsThe point is not the folder names, the point is the direction of dependency.
Routes compose Features, Features use Services and Utilities, Services do not know about Features, Utilities do not know about the app, that one-way direction keeps the project from slowly turning into a knot.
When I add something new, I want the location to be obvious. If it is a page, it belongs in routing, if it is behavior for one part of the product, it belongs in a feature, if it talks to an outside system, it belongs in a service, if it is pure reusable logic, it belongs in utilities.
That decision tree removes a surprising amount of noise.
The App Router makes it easy to put everything in route files. Fetching, layout, validation, UI, metadata, and mutations can all end up sitting next to the URL.
That works until the page stops being small.
I prefer route files that mostly answer one question: what screen or document should this URL render? The actual behavior lives somewhere with a business name, not a URL name.
That matters because URLs change. Navigation changes. A page can be split, merged, redesigned, or moved. The feature should not be glued to the route just because that is where the first version happened to land.
A feature folder is where I put the things that change together.
A contact feature might have a form component, a validation schema, a server action, and tests. A writing feature might have filtering, list rendering, detail rendering, and list parameter parsing. A project feature might have search, categories, cards, and detail views.
The exact files depend on the feature. I do not create empty architecture ceremony just to feel organized. If a feature does not need client state, it does not get a hook. If it does not mutate data, it does not get an action. If it does not need validation, it does not get a schema.
Structure should appear when the project asks for it.
The service layer is the part I care about most.
Any time the app talks to an external system, I want that conversation behind a small typed wrapper. CMS reads, email sending, rate limiting, bot protection, file storage, analytics, payments, search, whatever the project needs. The feature should call a function that describes the job. It should not care about the SDK ceremony underneath.
This gives me three things.
This is where a lot of small projects get messy. They start by importing an SDK wherever it is convenient. Then the SDK becomes part of every decision. By the time the project needs to change providers, add retries, improve error handling, or mock the integration in tests, the integration is everywhere.
A service layer is boring in the best way. It makes the outside world explicit.
React Server Components make the boundary visible, but visibility does not automatically make it safe.
My default is server-first. A component stays on the server unless it needs browser state, browser APIs, event handlers, or a client-only library. If only one small part is interactive, only that leaf becomes client-side.
This keeps the client bundle smaller, but that is not the only reason. It also keeps sensitive work away from code that can be imported by the browser. Anything touching secrets, write credentials, private tokens, or server-only behavior should be structurally impossible to pull into a client component.
I do not want safety to depend on memory. I want the project shape to make the wrong import feel wrong.
For form submissions and app-owned mutations, I prefer one clear path: a server action inside the feature.
API routes still have a place. Webhooks need URLs. External services need stable endpoints. Health checks need something machines can call. But I do not want the same logical flow split between a server action and a route handler just because both are available.
A generic contact flow, for example, can be described without tying it to any specific codebase:
The exact services can change. The architecture should not.
I do not like reading environment variables throughout an application.
It hides requirements. It spreads parsing rules. It makes deployment behavior harder to reason about. A string that looks optional in one file may actually be required in production somewhere else.
I prefer a central configuration module with named getters and validation. The rest of the app asks for meaning, not raw variables. It asks for the public site URL, not some loose environment string. It asks for the rate limit settings, not two separate values it has to parse again.
That sounds small until a deploy fails for a good reason instead of a user finding the problem later.
The main benefit is not beauty. It is pressure management.
As a project grows, pressure builds in predictable places. Pages become too large. Shared components become too smart. Integrations leak into UI. Client components accidentally pull server assumptions with them. Tests become hard to place because the behavior is smeared across layers.
This architecture gives that pressure somewhere to go.
There is a place for things to mature without pretending everything is reusable on day one.
This is more structure than a tiny static site needs.
If your site has one page, no CMS, no forms, no external services, no authenticated surface, and no planned growth, this can be overkill. You can keep the project simpler. You should keep it simpler.
The cost is discipline. You have to respect import direction. You have to stop yourself from calling an SDK directly in a random component because it is faster that day. You have to name features in a way that matches the product, not just the route. You have to decide when something is actually shared instead of promoting it too early.
That cost is real. I still think it is cheaper than cleaning up a project where every page invented its own architecture.
I chose this structure because I wanted the site to be honest software, not a pretty folder of pages.
A personal website is still a product surface. It represents how I think, how I write, how I ship, and how I maintain things after the first push. The architecture is part of that. It shows whether I treat small projects as disposable or as places to practice the same judgment I would bring to larger systems.
I do not need every project to be enterprise-shaped. I do need every project to have a reason for its shape.
For this website, the reason was clear: it has enough real behavior that boundaries matter. So I gave the boundaries names.
Do not copy my stack blindly.
Next.js, React, and TypeScript are strong defaults for a lot of websites. I like them. But the valuable part is not the logo set. The valuable part is the architecture underneath the tools.
Keep routing thin. Group behavior by feature. Put external systems behind services. Keep pure utilities pure. Treat the server/client boundary as a real boundary. Use one mutation path unless there is a good reason not to. Let shared components earn their place.
If your project is small, start small. But if your website is already doing real work, give it a structure that can hold real work.
That is the architecture I used here. Not because it is fancy. Because it keeps the boring parts boring, and that is usually where good software starts.