# How to Implement Feature-Based Module Architecture in Next.js: A Complete Guide

> Learn to implement feature-based module architecture in Next.js by organizing domain logic into self-contained packages. Streamline your project structure for better scalability.

- Repository: [Muhammad Arifin/fullstack-next-cloudflare](https://github.com/ifindev/fullstack-next-cloudflare)
- Tags: how-to-guide
- Published: 2026-03-04

---

**Implement feature-based module architecture by organizing domain-specific logic into self-contained packages under `src/modules/<feature>`, each containing routes, pages, components, actions, and schemas that are centrally exported via [`src/db/schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/schema.ts) and integrated into Next.js through thin router wrappers.**

The `ifindev/fullstack-next-cloudflare` template demonstrates a scalable approach to structuring full-stack Next.js applications by grouping related functionality into cohesive feature modules rather than technical layers. This pattern encapsulates everything a domain needs—from UI components to database schemas—within a single directory, making the codebase maintainable as it grows. By following this architecture, you ensure that adding or removing features requires changes to only one isolated location.

## What is Feature-Based Module Architecture?

Feature-based module architecture organizes code by **domain capability** rather than by technical role. Instead of scattering React components in `components/`, database logic in `db/`, and API handlers in `app/api/`, every domain (such as `auth`, `todos`, or `dashboard`) lives as a self-contained unit under `src/modules/<feature-name>`.

According to the `ifindev/fullstack-next-cloudflare` source code, each module encapsulates:

- **Route builders** for centralized URL management
- **Page components** exported as React components
- **UI components** scoped to the feature
- **Action handlers** for server-side CRUD operations
- **Schemas/Models** using Drizzle ORM and Zod validation
- **Utilities and enums** for feature-specific logic

This approach creates **high cohesion within features and loose coupling between them**, allowing teams to work on separate domains without merge conflicts or circular dependencies.

## Project Structure Overview

The template establishes a strict convention where all business logic resides in `src/modules/`, while the `app/` directory contains only thin routing wrappers.

```

src/
├── app/                    # Next.js App Router entry points (thin wrappers)

│   └── dashboard/
│       └── todos/
│           └── page.tsx    # Simply imports from modules/todos/

├── db/
│   └── schema.ts           # Re-exports all Drizzle tables from modules

├── lib/                    # Shared infrastructure (DB client, etc.)

└── modules/                # Domain-specific features

    ├── auth/               # Authentication feature

    │   ├── auth.route.ts
    │   ├── actions/
    │   └── schemas/
    └── todos/              # Todos feature

        ├── todos.route.ts
        ├── todo-list.page.tsx
        ├── components/
        ├── actions/
        └── schemas/

```

All imports use the **`@` alias** configured in [`tsconfig.json`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/tsconfig.json), pointing to `src/` to eliminate relative path hell and keep imports stable during refactoring.

## Core Components of a Feature Module

### Route Builders

Each feature exports a route builder object that centralizes URL constants, preventing magic strings throughout the application. In [`src/modules/todos/todos.route.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/todos.route.ts), the pattern supports both static and dynamic segments:

```typescript
// src/modules/todos/todos.route.ts
const todosRoutes = {
  list: "/dashboard/todos",
  new: "/dashboard/todos/new",
  edit: (id: string | number) => `/dashboard/todos/${id}/edit`,
} as const;

export default todosRoutes;

```

This enables type-safe navigation—change a route in one place, and TypeScript immediately highlights all affected components.

### Page Components

Features export their pages as standard React components, completely decoupled from Next.js routing. The file [`src/modules/todos/todo-list.page.tsx`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/todo-list.page.tsx) contains the actual page implementation:

```tsx
// src/modules/todos/todo-list.page.tsx
import { getAllTodos } from "@/modules/todos/actions/get-todos.action";
import { TodoCard } from "./components/todo-card";

export default async function TodoListPage() {
  const todos = await getAllTodos();
  return (
    <div>
      {todos.map((t) => (
        <TodoCard key={t.id} todo={t} />
      ))}
    </div>
  );
}

```

### UI Components

Reusable building blocks that belong exclusively to a feature live in `src/modules/<feature>/components/`. These components remain private to the feature unless explicitly exported, preventing global namespace pollution. For example, [`src/modules/todos/components/todo-card.tsx`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/components/todo-card.tsx) renders individual todo items using the feature's own types.

### Action Handlers

Server-side logic resides in `src/modules/<feature>/actions/` and handles CRUD operations, validation, and authorization. The [`src/modules/todos/actions/create-todo.action.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/actions/create-todo.action.ts) file demonstrates the pattern:

```typescript
// src/modules/todos/actions/create-todo.action.ts
import { requireAuth } from "@/modules/auth/utils/auth-utils";
import { todos, insertTodoSchema } from "@/modules/todos/schemas/todo.schema";
import { db } from "@/lib/db";

export const createTodo = async (input: typeof insertTodoSchema) => {
  const { user } = await requireAuth();
  const validated = await insertTodoSchema.parseAsync({ 
    ...input, 
    userId: user.id 
  });
  return db.insert(todos).values(validated).run();
};

```

Action handlers import type definitions directly from their feature's schemas, ensuring end-to-end type safety from database to UI.

### Schemas and Models

Database tables and validation schemas coexist in `src/modules/<feature>/schemas/`. Using Drizzle ORM with Zod integration, [`src/modules/todos/schemas/todo.schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/schemas/todo.schema.ts) defines both the SQL structure and runtime validation:

```typescript
// src/modules/todos/schemas/todo.schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";

export const todos = sqliteTable("todos", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  status: text("status").notNull(),
  userId: integer("user_id").notNull(),
});

export const insertTodoSchema = createInsertSchema(todos);
export const selectTodoSchema = createSelectSchema(todos);
export const updateTodoSchema = insertTodoSchema.partial().omit({ id: true });

```

### Utilities and Enums

Feature-specific helpers and constants live in `src/modules/<feature>/utils/` and `src/modules/<feature>/models/`. The [`src/modules/auth/utils/auth-utils.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/auth/utils/auth-utils.ts) file contains authentication guards used by other features' action handlers, while [`src/modules/todos/models/todo.enum.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/models/todo.enum.ts) defines status enumerations.

## Centralized Schema Management

While each feature owns its database definitions, a central [`src/db/schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/schema.ts) aggregates all tables for the Drizzle ORM client:

```typescript
// src/db/schema.ts
import { authUser } from "@/modules/auth/schemas/auth.schema";
export { categories } from "@/modules/todos/schemas/category.schema";
export { todos } from "@/modules/todos/schemas/todo.schema";
// Additional feature exports...

```

This single entry point allows the database client to instantiate once with complete type information, while preserving feature modularity. When adding a new feature, you simply append one export line to this file.

## Routing Integration with Next.js App Router

The `app/` directory remains minimal, serving only as a routing layer that delegates to feature modules. This preserves clean separation between Next.js routing mechanics and business logic:

```tsx
// src/app/dashboard/todos/page.tsx
import TodoListPage from "@/modules/todos/todo-list.page";

export default function Page() {
  return <TodoListPage />;
}

```

For dynamic routes, the pattern remains identical—import the feature's page component and pass route parameters through.

## Step-by-Step: Adding a New Feature

To implement a "Notes" feature following this architecture, create the following structure:

```

src/
└─ modules/
   └─ notes/
      ├─ notes.route.ts
      ├─ notes.page.tsx
      ├─ components/
      │  └─ note-card.tsx
      ├─ actions/
      │  ├─ get-notes.action.ts
      │  └─ create-note.action.ts
      └─ schemas/
         └─ note.schema.ts

```

**1. Define Routes**

```typescript
// src/modules/notes/notes.route.ts
const notesRoutes = {
  list: "/dashboard/notes",
  new: "/dashboard/notes/new",
  edit: (id: string | number) => `/dashboard/notes/${id}/edit`,
} as const;

export default notesRoutes;

```

**2. Create Schema**

```typescript
// src/modules/notes/schemas/note.schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";

export const notes = sqliteTable("notes", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  content: text("content"),
  userId: integer("user_id").notNull(),
});

export const insertNoteSchema = createInsertSchema(notes);
export const selectNoteSchema = createSelectSchema(notes);

```

**3. Implement Action Handler**

```typescript
// src/modules/notes/actions/create-note.action.ts
import { requireAuth } from "@/modules/auth/utils/auth-utils";
import { notes, insertNoteSchema } from "@/modules/notes/schemas/note.schema";
import { db } from "@/lib/db";

export const createNote = async (input: typeof insertNoteSchema) => {
  const { user } = await requireAuth();
  const validated = await insertNoteSchema.parseAsync({ 
    ...input, 
    userId: user.id 
  });
  return db.insert(notes).values(validated).run();
};

```

**4. Build Page Component**

```tsx
// src/modules/notes/notes.page.tsx
import { getAllNotes } from "@/modules/notes/actions/get-notes.action";
import { NoteCard } from "./components/note-card";

export default async function NotesPage() {
  const notes = await getAllNotes();
  return (
    <div>
      {notes.map((n) => (
        <NoteCard key={n.id} note={n} />
      ))}
    </div>
  );
}

```

**5. Wire to Router**

```tsx
// src/app/dashboard/notes/page.tsx
import NotesPage from "@/modules/notes/notes.page";

export default function Page() {
  return <NotesPage />;
}

```

**6. Export Schema**

Add to [`src/db/schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/schema.ts):

```typescript
export { notes } from "@/modules/notes/schemas/note.schema";

```

**7. Consume in Navigation**

```tsx
// src/components/navigation.tsx
import Link from "next/link";
import notesRoutes from "@/modules/notes/notes.route";

<Link href={notesRoutes.list}>Notes</Link>

```

## Summary

- **Encapsulation**: Place all domain files—routes, pages, components, actions, and schemas—under `src/modules/<feature>` to create self-contained units that are easy to locate and modify.
- **Scalability**: Adding features requires only creating a new module directory and registering its schema export in [`src/db/schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/schema.ts), without touching unrelated code.
- **Type Safety**: The `@` path alias and centralized schema exports enable TypeScript to infer types across the entire stack, from database queries to React props.
- **Clean Routing**: Keep Next.js `app/` directory thin by importing and rendering feature page components, maintaining separation between framework routing and business logic.
- **Centralized URLs**: Use route builder objects to eliminate magic strings and enable compile-time checking of navigation paths.

## Frequently Asked Questions

### What are the benefits of feature-based architecture over folder-by-type?

**Feature-based architecture colocates related code by domain capability rather than technical role**, eliminating the need to jump between `components/`, `lib/`, and `app/` directories when working on a single user story. As demonstrated in `ifindev/fullstack-next-cloudflare`, this reduces merge conflicts when multiple developers work on different features and makes deletion of obsolete functionality as simple as removing one directory.

### How does the template handle database schema changes across features?

**Each feature owns its Drizzle ORM table definitions**, but re-exports them through [`src/db/schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/schema.ts) to instantiate a single database client with complete type information. When you modify [`src/modules/todos/schemas/todo.schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/schemas/todo.schema.ts), TypeScript immediately reflects those changes in [`src/modules/todos/actions/create-todo.action.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/actions/create-todo.action.ts) because both import from the same source, while the central schema file ensures the database client stays synchronized.

### Can components from one feature be used in another feature?

**Yes, but with explicit imports that maintain boundary visibility.** Since all modules use the `@` alias, you can import `TodoCard` from the todos module into the dashboard module using `@/modules/todos/components/todo-card`. However, the architecture encourages keeping such cross-feature dependencies minimal and well-defined, preserving the encapsulation benefits while allowing justified reuse.

### How do route builders improve type safety compared to hardcoded strings?

**Route builders centralize URL definitions as const objects with TypeScript `as const` assertions**, turning runtime string errors into compile-time failures. When you change `todosRoutes.list` in [`src/modules/todos/todos.route.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/todos.route.ts), TypeScript immediately flags all navigation components using the old path, whereas hardcoded strings would fail silently at runtime or require global search-and-replace operations.