Use app/ for routes and composition, src/features/ for product behavior, src/lib/ for provider adapters, and src/shared/ only for code that is genuinely reusable. That split gives a Next.js SaaS a place to grow without turning page files into the place where authentication, billing, Firestore access, and UI state get tangled together.
For a solo technical founder, the payoff is not an academic folder taxonomy. It is being able to ask an AI coding agent for a new SaaS capability, review the change in a predictable area, and keep Stripe or Firebase details out of unrelated features. The structure below is a real, current shape from the Frontend Accelerator product repository, adapted into a practical pattern rather than a promise that every SaaS needs the same folders.
The folder structure to start with
Next.js leaves project organization largely up to you: app is the App Router, public serves static assets, and src is an optional source folder. A route becomes accessible when a segment contains a page or route file. That flexibility is useful, but it also means a SaaS needs its own boundary rules before feature work accumulates.
app/
[locale]/ localized marketing, auth, and protected routes
api/ auth, lead-capture, payment, and webhook handlers
src/
features/ auth, blog, changelog, dashboard, glossary,
marketing, and profile behavior
shared/ reusable components, hooks, layouts, and utilities
lib/ auth, AI, database, email, and payment adapters
config/ application and sitemap configuration
i18n/ routing, request configuration, and messages
services/ shared API and state services
styles/ global styles and design tokens
types/ global TypeScript declarations
public/ static assets
This is a feature-first structure, not a claim that folders alone make an app production-ready. The useful part is the dependency direction: route files compose; features own product behavior; provider integrations sit behind narrow infrastructure adapters.
Architecture diagram: the dependency direction that matters
app/[locale] and app/api
│ route composition, request entry points
▼
src/features/<feature>
│ feature actions, components, validation, product rules
├──────────────► src/shared/ only cross-feature reuse
▼
src/lib/<domain>/adapter
│ provider-specific implementation remains here
▼
Firestore / MongoDB / Stripe / Lemon Squeezy / AI provider
The arrow should not point directly from a feature to a provider SDK. In Accelerator, database work is accessed through a database adapter and repositories; payments through a payment adapter; and AI behavior through an AI adapter. The concrete Firestore, MongoDB, Stripe, Lemon Squeezy, OpenAI, Claude, and Gemini integrations live in adapter folders. That gives the rest of the application a smaller, more stable surface to depend on.
What belongs in app/
Keep app/ intentionally thin. It owns URL structure, layouts, route groups, route handlers, loading and error boundaries, and composition of the UI for a route. In the verified product repository, localized public, auth, and protected routes live under app/[locale]/; API routes cover authentication, lead capture, payments, and payment webhooks.
Do not turn a page into a hidden service layer. A dashboard page can fetch the data it needs and compose feature components, but it should not grow a second copy of billing rules or scattered Firebase calls. Next.js route groups can organize layouts without affecting the URL, and private folders can colocate non-routable utilities when a route needs them. Use those framework features for route-local concerns; use feature modules for product behavior that must survive beyond one page.
Server Components are the default boundary
In an App Router project, start with Server Components and introduce "use client" only where hooks, event handlers, browser APIs, or client-only libraries require it. This matters for folder structure because a Client Component must not become a shortcut around server-only infrastructure. Keep database and payment setup on the server side, then pass narrow data and callbacks into interactive components.
What belongs in src/features/
A feature directory owns behavior that a user recognizes: authentication, the blog, a dashboard capability, or profile management. It can contain focused components, server actions, validation, types that are specific to the feature, and small local utilities. The key rule is that one feature should not reach into another feature’s internals. If code truly becomes reusable across independent features, promote it to src/shared/; otherwise keep it close to the behavior it supports.
This is especially valuable when working with agents. A task such as “add a customer-facing subscription status to the dashboard” can be constrained to the dashboard feature, the payment boundary, and the relevant route. That is much safer to review than a broad instruction such as “update the dashboard everywhere.” The architectural rule becomes a practical review checklist.
What belongs in src/lib/: provider boundaries
Treat src/lib/ as protected infrastructure. In the product repository, it holds auth, AI, database, email, and payment domains. Provider-specific code stays below those domains’ adapter folders. Features do not import Stripe, Firebase, MongoDB, or another provider SDK directly.
For example, a subscription feature should express what it needs from a payment adapter—such as creating a checkout session or reconciling a verified event—not how the Stripe SDK formats a request. A Firestore-backed feature should use the database adapter and repository shape rather than query a collection from a component. This is not abstraction for its own sake: it localizes a provider change, makes tests more targeted, and prevents the client bundle from accidentally reaching server credentials.
An illustrative addition workflow
- Define the reader-visible behavior in the relevant
src/features/<feature>directory. - Decide whether the request enters through a Server Action or a Route Handler; keep the route file as the request boundary.
- Use the existing database or payment adapter contract. If it lacks a needed operation, extend the domain boundary deliberately before adding provider code.
- Keep provider SDK calls inside
src/lib/**/adapters/. - Add a focused Jest or Testing Library test for the feature rule and an expected failure path.
- Run the project’s lint, test, and build checks before treating the change as done.
This workflow is a recommendation based on the repository’s documented architecture; it is not an assertion that a particular implementation has already been tested for your product.
Where src/shared/ helps—and where it hurts
src/shared/ is useful for UI primitives, layouts, generic hooks, and utilities used by multiple features. It becomes harmful when it is used as a parking lot for feature-specific logic. A “shared” folder full of one-off checkout helpers, dashboard policy checks, and blog-only types obscures ownership and invites cross-feature coupling.
Use this decision rule: if deleting a feature would make the module irrelevant, keep it inside that feature. If two independently evolving features need it and the API can stay generic, move it to src/shared/. Keep global configuration, provider setup, and top-level TypeScript declarations in their explicit homes rather than importing them through a generic utility layer.
Four failure modes this structure prevents
- Provider leakage: a React component imports a payment or database SDK, which blurs server/client boundaries and makes later replacement harder.
- Route logic sprawl: a page file owns validation, authorization, persistence, and presentation, so a second entry point duplicates the rules.
- Feature-to-feature imports: one feature reaches into another’s private components or data details, making a local refactor unexpectedly risky.
- Shared-folder gravity: every unsure module is placed in
shared, leaving no clear owner and no safe place for agents to make changes.
A practical review test
Before merging an AI-assisted change, trace the imports from the changed page or Route Handler. You should be able to explain which feature owns the rule, which server-side boundary validates it, and which adapter reaches the provider. If a component jumps straight to Firestore or Stripe, or a feature imports another feature’s internal component, stop and move the responsibility to its proper boundary. This quick trace catches structural drift before it becomes a costly cleanup.
When this structure is the wrong choice
A tiny one-screen prototype does not need every folder shown here. Start smaller if there is no meaningful feature boundary yet. Conversely, this is not an organization-based multi-tenant or enterprise-permissions architecture; those needs require deliberate product and authorization design beyond a folder tree. The structure also does not replace security review, webhook verification, tests, monitoring, or product-specific decisions.
For a TypeScript SaaS with authentication, payment state, data access, and a growing dashboard, though, defining these boundaries early is usually cheaper than unpicking them after several AI-assisted feature passes. Start from a codebase whose routes, feature modules, and providers have a clear relationship—then keep the relationship intact as you build.
View the reference architecture
Frontend Accelerator is a Next.js SaaS foundation that uses the feature-first boundaries described here, along with database, payment, and AI adapters. It is a fit for solo developers and freelancers who want an understandable starting point to extend; it does not replace product-specific engineering or testing. View Accelerator’s architecture.


