Skip to main content
The web app uses a feature-based architecture to keep code organized and scalable as your application grows.

Why Feature-Based?

Co-location

Keep all code for a feature together

Scalability

Add features without reorganizing

Team Collaboration

Multiple devs can work on different features

Code Splitting

Natural boundaries for lazy loading

Feature Structure

Each feature is a self-contained module:
features/
└── [feature-name]/
    ├── [feature]-page.tsx      # Main page component
    ├── [feature]-*.tsx         # Feature-specific components
    ├── use-[feature].ts        # Feature-specific hooks
    ├── [feature]-store.ts      # Zustand store (if needed)
    ├── [feature]-types.ts      # TypeScript types
    └── [feature]-api.ts        # API calls
Not all features need all files. Start simple and add complexity as needed.

Example: Home Feature

The starter includes a simple home feature:
features/
└── home/
    └── home-page.tsx

Home Page Component

features/home/home-page.tsx
"use client";

import { Heading } from "@/components/ui/heading";
import Image from "next/image";

export const HomePage = () => {
  return (
    <div className="mx-auto max-w-2xl h-screen flex flex-col justify-center gap-6">
      <Image src="/copilot-cli.svg" alt="Copilot CLI" width={60} height={60} />
      <Heading variant="h1" className="">
        Ship faster. Same team.
      </Heading>
      <p className="text-lg text-muted-foreground text-balance">
        GitHub Copilot Agents and Skills that accelerate modern frontend development.
      </p>
    </div>
  );
};

Connecting to Routes

Route files in app/ are thin wrappers:
app/page.tsx
import { HomePage } from "@/features/home/home-page";

export default function Home() {
  return <HomePage />;
}
This separation keeps routing logic in app/ and feature logic in features/, making it easy to refactor routes without touching feature code.

Building a New Feature

Let’s build a dashboard feature from scratch.
1

Create the feature directory

mkdir -p features/dashboard
2

Add the main page component

features/dashboard/dashboard-page.tsx
"use client";

import { Heading } from "@/components/ui/heading";
import { DashboardStats } from "./dashboard-stats";
import { DashboardActivity } from "./dashboard-activity";

export function DashboardPage() {
  return (
    <div className="container py-8">
      <Heading variant="h1">Dashboard</Heading>
      <div className="mt-8 grid gap-6 md:grid-cols-3">
        <DashboardStats />
      </div>
      <div className="mt-8">
        <DashboardActivity />
      </div>
    </div>
  );
}
3

Add feature-specific components

features/dashboard/dashboard-stats.tsx
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";

export function DashboardStats() {
  return (
    <>
      <Card>
        <CardHeader>
          <CardTitle>Total Users</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="text-3xl font-bold">1,234</div>
        </CardContent>
      </Card>
      {/* More stat cards */}
    </>
  );
}
features/dashboard/dashboard-activity.tsx
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";

export function DashboardActivity() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Recent Activity</CardTitle>
      </CardHeader>
      <CardContent>
        {/* Activity list */}
      </CardContent>
    </Card>
  );
}
4

Create the route

mkdir -p app/dashboard
app/dashboard/page.tsx
import { DashboardPage } from "@/features/dashboard/dashboard-page";

export default function Dashboard() {
  return <DashboardPage />;
}
5

Add metadata (optional)

app/dashboard/page.tsx
import type { Metadata } from "next";
import { DashboardPage } from "@/features/dashboard/dashboard-page";

export const metadata: Metadata = {
  title: "Dashboard",
  description: "View your analytics and activity",
};

export default function Dashboard() {
  return <DashboardPage />;
}

Feature with State Management

For features with complex state, add a Zustand store:
features/
└── todos/
    ├── todos-page.tsx
    ├── todos-list.tsx
    ├── todos-form.tsx
    ├── todos-store.ts          # ← Zustand store
    └── todos-types.ts

Example Store

features/todos/todos-store.ts
import { create } from 'zustand';
import type { Todo } from './todos-types';

interface TodosStore {
  todos: Todo[];
  addTodo: (text: string) => void;
  toggleTodo: (id: string) => void;
  deleteTodo: (id: string) => void;
}

export const useTodosStore = create<TodosStore>((set) => ({
  todos: [],
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: crypto.randomUUID(), text, completed: false }]
  })),
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  })),
  deleteTodo: (id) => set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id)
  })),
}));

Using the Store

features/todos/todos-page.tsx
"use client";

import { useTodosStore } from './todos-store';
import { TodosList } from './todos-list';
import { TodosForm } from './todos-form';

export function TodosPage() {
  const todos = useTodosStore(state => state.todos);
  
  return (
    <div className="container py-8">
      <TodosForm />
      <TodosList todos={todos} />
    </div>
  );
}
See the State Management guide for more Zustand patterns.

Feature with API Integration

For features that fetch data, add an API utility file:
features/posts/posts-api.ts
const API_BASE = process.env.NEXT_PUBLIC_API_URL;

export async function getPosts() {
  const res = await fetch(`${API_BASE}/posts`);
  if (!res.ok) throw new Error('Failed to fetch posts');
  return res.json();
}

export async function getPost(id: string) {
  const res = await fetch(`${API_BASE}/posts/${id}`);
  if (!res.ok) throw new Error('Failed to fetch post');
  return res.json();
}

export async function createPost(data: CreatePostData) {
  const res = await fetch(`${API_BASE}/posts`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  if (!res.ok) throw new Error('Failed to create post');
  return res.json();
}
Fetch data in Server Components for better performance:
features/posts/posts-page.tsx
import { getPosts } from './posts-api';
import { PostsList } from './posts-list';

export async function PostsPage() {
  const posts = await getPosts();
  
  return (
    <div className="container py-8">
      <h1>Posts</h1>
      <PostsList posts={posts} />
    </div>
  );
}

Client Components (When Needed)

For interactive features, use client components with hooks:
features/posts/posts-page.tsx
"use client";

import { useEffect, useState } from 'react';
import { getPosts } from './posts-api';

export function PostsPage() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    getPosts()
      .then(setPosts)
      .finally(() => setLoading(false));
  }, []);
  
  if (loading) return <div>Loading...</div>;
  
  return <div>{/* Render posts */}</div>;
}

Feature Organization Tips

Consistent prefixes help identify feature files:
features/dashboard/
├── dashboard-page.tsx
├── dashboard-stats.tsx
├── dashboard-chart.tsx
├── dashboard-store.ts
└── dashboard-types.ts
Avoid generic names like index.tsx or component.tsx.

Best Practices

Single Responsibility

Each feature should represent one user-facing capability

Self-Contained

Features should not depend on other features

Thin Routes

Keep app/ routes minimal, delegate to features

Start Simple

Add complexity (stores, types, API) as needed

Next Steps

State Management

Learn Zustand patterns for complex state

Components

Build reusable UI components

Build docs developers (and LLMs) love