TL;DR: Feature-first architecture organizes code around product capabilities such as auth, billing, onboarding, reporting, and admin. It is useful when features have their own UI, API, validation, tests, and ownership. The risk is creating isolated folders with duplicated shared logic.
Why Teams Move to Feature-First
Most codebases start organised by technical layer: a components folder, a services folder, a utils folder. That works until a single product change touches six folders and three teams. Feature-first architecture is the fix teams reach for when a growing React, Next.js, Flutter, or Node.js codebase has become slow and risky to change.
Layer-First vs Feature-First
| Structure | Example Folders | Best For |
|---|---|---|
| Layer-first | components, hooks, services, models | small apps and simple teams |
| Feature-first | auth, billing, reports, admin | growing products with clear domains |
| Hybrid | features plus shared platform modules | most production SaaS apps |
Example Folder Structure
src/
features/
auth/
components/
api/
validation/
tests/
billing/
components/
api/
jobs/
tests/
reports/
components/
queries/
export/
shared/
ui/
config/
database/
observability/
Feature-First in a Next.js App Router Project
In the App Router, the app/ directory is the routing table, so keep it thin: a route file should load data and render a feature component, nothing more. The feature owns everything else.
app/
(dashboard)/
billing/
page.tsx // imports from features/billing, no business logic
reports/
page.tsx
src/
features/
billing/
components/ // InvoiceTable, PlanPicker
server/ // server actions and queries for billing only
schema.ts // Zod schemas shared by form and server action
index.ts // the feature's public API
reports/
...
shared/
ui/ // design system primitives
lib/ // http client, logger, date helpers
// app/(dashboard)/billing/page.tsx
import { BillingOverview, getBillingSummary } from '@/features/billing';
export default async function BillingPage() {
const summary = await getBillingSummary();
return <BillingOverview summary={summary} />;
}
The route knows nothing about invoices or plans. Moving billing to a different URL, or reusing it in an admin area, is a two-line change.
Feature-First on a Node.js Backend
The same idea applies to an API. Group the route handler, validation, service logic, and data access for one capability together, instead of spreading them across controllers/, services/, and repositories/ folders:
src/
modules/
billing/
billing.routes.ts
billing.service.ts
billing.repository.ts
billing.schema.ts
billing.test.ts
index.ts
accounts/
...
platform/
db.ts
auth-middleware.ts
logger.ts
Other modules call billing through its index.ts, never by reaching into billing.repository.ts. That one rule is what keeps a modular monolith from turning back into a tangle, and it is also what makes extracting a module into its own service possible later.
Feature-First in Flutter
Flutter apps benefit from the same structure: each feature holds its screens, widgets, state providers, and repositories. We cover the Flutter-specific version, with Riverpod providers and per-feature routing, in Feature-First Flutter Architecture with Riverpod, DDD and GoRouter.
Give Every Feature a Public API
Each feature exports what other code may use from a single index.ts. Everything else is private by convention:
// src/features/billing/index.ts
export { BillingOverview } from './components/BillingOverview';
export { getBillingSummary } from './server/queries';
export type { BillingSummary } from './schema';
Conventions erode under deadline pressure, so enforce the boundary in lint. ESLint's built-in no-restricted-imports rule is enough to stop deep imports into another feature:
// eslint.config.js (excerpt)
export default [
{
files: ['src/features/**'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@/features/*/*'],
message: 'Import other features through their index.ts public API.',
},
],
},
],
},
},
];
For larger codebases, eslint-plugin-boundaries can also forbid shared/ from importing features and catch circular dependencies between features.
Why Feature-First Works
Feature-first architecture makes product ownership visible. If billing breaks, you know where to look. If onboarding changes, the UI, API calls, validation, and tests are close to the business workflow.
What Belongs In A Feature?
| Belongs In Feature | Belongs In Shared |
|---|---|
| feature-specific UI | generic buttons, inputs, layout primitives |
| feature API client | HTTP client base wrapper |
| feature validation schema | reusable validation helpers |
| feature tests | test utilities |
| feature copy and states | global design tokens |
Common Mistakes
- putting everything into features and duplicating shared logic,
- creating one huge "common" folder that hides ownership,
- splitting too early before the product has stable domains,
- mixing business logic into UI components,
- having feature folders with no tests or ownership.
Practical Rules
- Start with product language, not framework language.
- Keep shared code boring and small.
- Move code to shared only after two or three real uses.
- Keep tests near the feature they protect.
- Document feature boundaries in the README.
- Avoid circular imports between features.
Migrating an Existing Codebase Step by Step
You do not need a rewrite. Move one feature at a time:
- Pick the product area that changes most often, such as billing or onboarding.
- Create
features/<name>/and move its components, hooks, API calls, schemas, and tests into it together. - Add the feature's
index.ts, and update imports elsewhere to use it. - Turn on the lint rule for that feature only.
- Ship, then repeat for the next area. Leave stable, rarely touched code where it is until there is a reason to move it.
Most teams see the benefit after the first two or three features, because the areas they touch every week are the first to become easy to change.
When Feature-First Is A Bad Fit
Do not force feature-first architecture into a tiny landing page or prototype. It adds ceremony when the product has not found stable domains yet. For small projects, a simple layer structure can be faster.
When SoftwareCrafting Can Help
We use feature-first and modular architecture when building SaaS, mobile apps, dashboards, and backend systems that need to stay maintainable after launch. See our full-stack web development services or request a software audit.
Frequently Asked Questions
Is feature-first architecture only for frontend?
No. It can work for frontend, backend, mobile, and full-stack applications when product capabilities are clear.
Is feature-first better than clean architecture?
They solve different problems. Feature-first organizes by product area. Clean architecture organizes dependency direction and business rules.
Should shared components live inside features?
Only when they are truly feature-specific. Generic UI primitives belong in shared UI.
Can feature-first architecture work with Next.js App Router?
Yes. Keep route files thin and move feature logic into feature modules where it can be tested and reused.
How do I stop features importing each other's internals?
Export a public API from each feature's index.ts and enforce it with ESLint's no-restricted-imports rule or eslint-plugin-boundaries, so deep imports fail in CI instead of in code review.
How do I migrate to feature-first?
Start with one high-change product area, move related UI/API/tests together, and avoid a full codebase rewrite.
