Software Crafting

Feature-First Architecture: How to Organize Frontend and Backend Code by Product Area

DDeepak RajputWeb Development12 min read31 Jul 2026Updated 2026-10-26
Feature-first architecture folder map organized by product capabilities

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

StructureExample FoldersBest For
Layer-firstcomponents, hooks, services, modelssmall apps and simple teams
Feature-firstauth, billing, reports, admingrowing products with clear domains
Hybridfeatures plus shared platform modulesmost 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 FeatureBelongs In Shared
feature-specific UIgeneric buttons, inputs, layout primitives
feature API clientHTTP client base wrapper
feature validation schemareusable validation helpers
feature teststest utilities
feature copy and statesglobal 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

  1. Start with product language, not framework language.
  2. Keep shared code boring and small.
  3. Move code to shared only after two or three real uses.
  4. Keep tests near the feature they protect.
  5. Document feature boundaries in the README.
  6. Avoid circular imports between features.

Migrating an Existing Codebase Step by Step

You do not need a rewrite. Move one feature at a time:

  1. Pick the product area that changes most often, such as billing or onboarding.
  2. Create features/<name>/ and move its components, hooks, API calls, schemas, and tests into it together.
  3. Add the feature's index.ts, and update imports elsewhere to use it.
  4. Turn on the lint rule for that feature only.
  5. 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.

About the author

Deepak Rajput

Founder and CEO of SoftwareCrafting

Deepak Rajput is the Founder and CEO of SoftwareCrafting in Delhi and writes about web, mobile, backend, cloud, security, and engineering leadership.

Writes about Next.js, Node.js, React Native, PostgreSQL, AWS, Kubernetes.

View author profile

Last updated: 2026-10-26