Best Practices for Structuring your Projects – 2. Feature Based Structure
This blog post is a translation of a Japanese article posted on May 28th, 2025.
Introduction
In application development, directory structure is a design element that directly impacts maintainability, scalability, and development efficiency.
This article introduces a feature-based structure that is easy to organize by function and robust against expansion, aimed at teams struggling with the following issues:
- Files become scattered and hard to manage as the project grows
- New members get lost, wondering where everything is
- A directory structure that started small tends to break down as it grows
⚠️ This structure is not the only correct one!
This article is just one example of a structural pattern that I have found to be relatively easy to handle, based on my experience in actual projects and team development.
Please customize and adapt the structure as appropriate based on your team’s size, technical level, and the nature of the project.
Structural Policy
- Separate directories by feature.
- Organize responsibilities within each feature by file (e.g.,
components.tsx,usecases.ts). - Compose logic that combines multiple features (screen UI / responses) on the outside.
- Place common logic in
shared/and the application foundation incore/.
Criteria for Separating Features
“What constitutes a single feature” significantly impacts the overall clarity and maintainability of the structure. Let’s design based on criteria like the following:
| Classification Criteria | Details |
|---|---|
| By Business Domain | Ex: user, product, order, dashboard |
| By Screen | A single screen with clear responsibilities (Ex: Login, Profile, Settings) |
| By API | Aligned with the REST/GraphQL endpoint structure |
| By Change Frequency / Team | Also effective for features that are frequently modified or when separating by the responsible team |
💡 The basic idea is to group the “screens, logic, and state that complete a single function” together.
If the boundaries are ambiguous, it’s realistic to start with broader divisions and split or merge them as needed.
Specific Examples
- Initially, group
login,signup, andforgotPasswordintoauth/.- → This makes it easier to manage auth-related UI, APIs, and state management collectively.
- User-related functions like
user,profile, andsettings…- → Can be placed under
user/at first. - → It’s also fine to split them out into independent
profile/orsettings/directories later.
- → Can be placed under
Example Directory Structure
Frontend
src/
├── features/ // Grouped by feature (UI, logic, state)
│ ├── user/
│ │ ├── components.tsx // UI components specific to the user feature
│ │ ├── hooks.ts // Custom hooks
│ │ ├── usecases.ts // Usecases (operations called from UI)
│ │ ├── repository.ts // Data fetching logic (API, localStorage, etc.)
│ │ └── store.ts // State management (Zustand, Redux, etc.)
│ ├── product/
│ └── order/
│
├── pages/ // Construct screens by combining multiple features
│ ├── dashboard.tsx
│ └── user.tsx
│
├── shared/ // Reusable UI / utilities
│ ├── components.tsx // Common UI (Button, Modal, etc.)
│ ├── utils.ts // General-purpose functions (pure functions)
│ ├── constants.ts // Constants
│ └── types.ts // Common types
│
├── core/ // App-wide foundational features
│ ├── api.ts // API client settings (axios, etc.)
│ ├── auth.ts // Authentication-related logic
│ ├── config.ts // Environment settings
│ └── store.ts // Global store initialization
│
└── main.tsx // Entry point
- Define feature-specific UI in each feature’s
components.tsx. - Write state management and API logic in
store.ts/hooks.ts/usecases.ts. - Freely combine these in
pages/to build screens.
💡 Key Design Points
→ Keep state, logic, and UI self-contained within features/*.
→ Combine multiple features in pages/ to build screen UI.
→ Be mindful of DRY by extracting common components, constants, and types into shared/.
→ Consolidate app-wide settings and state management in core/.
Assembling the UI (Example with a single feature)
// pages/user.tsx
import { UserList } from '@/features/user/components';
import { useUser } from '@/features/user/hooks';
export const UserPage = () => {
const { users } = useUser();
return <UserList users={users} />;
};
Assembling the UI (Example with multiple features)
// pages/dashboard.tsx
import { UserCard } from '@/features/user/components';
import { ProductCard } from '@/features/product/components';
import { useUser } from '@/features/user/hooks';
import { useProduct } from '@/features/product/hooks';
export const Dashboard = () => {
const { currentUser } = useUser();
const { featuredProduct } = useProduct();
return (
<>
<UserCard name={currentUser.name} />
<ProductCard product={featuredProduct} />
</>
);
};
Backend
src/
├── features/ // Domain logic for each feature
│ ├── user/
│ │ ├── usecases.ts // Usecases (application service layer)
│ │ ├── repository.ts // DB or external API access
│ │ └── types.ts // Type definitions
│ ├── product/
│ └── order/
│
├── routes/ // Routing by endpoint (response composition)
│ ├── user/route.ts // /api/users
│ └── dashboard/route.ts // /api/dashboard (combines multiple features)
│
├── shared/ // Common types, constants, functions
│ ├── utils.ts
│ ├── constants.ts
│ └── types.ts
│
├── core/ // App-wide initialization & common logic
│ ├── config.ts
│ ├── database.ts
│ ├── auth.ts
│ └── errorHandler.ts
│
└── main.ts // Entry point
💡 Key Design Points
→ features/* should focus on logic and data fetching (services, DB).
→ Consolidate endpoint handling in routes/*/route.ts.
→ Design flexibly by combining return values so that response structures can be freely composed.
→ Explicitly mount all routes in main.ts to centralize the app’s starting point.
Assembling the API (Example with a single feature)
// routes/user/route.ts
import { Router } from 'express';
import { getUserById } from '@/features/user/usecase';
const router = Router();
router.get('/:userId', async (req, res) => {
const user = await getUserById(req.params.userId);
res.json(user);
});
export default router;
Assembling the API (Example with multiple features)
// routes/dashboard/route.ts
import { Router } from 'express';
import { getUserById } from '@/features/user/usecase';
import { getOrdersByUserId } from '@/features/order/usecase';
import { getProductsByIds } from '@/features/product/usecase';
const router = Router();
router.get('/:userId', async (req, res) => {
const userId = req.params.userId;
const user = await getUserById(userId);
const orders = await getOrdersByUserId(userId);
const productIds = orders.flatMap((o) => o.productIds);
const products = await getProductsByIds(productIds);
res.json({
user: {
name: user.name,
email: user.email,
},
orderSummary: {
count: orders.length,
products,
},
});
});
export default router;
Example of combining routes
// main.ts
import express from 'express';
import dashboardRoute from './routes/dashboard/route';
import userRoute from './routes/user/route';
const app = express();
app.use(express.json());
app.use('/api/dashboard', dashboardRoute);
app.use('/api/users', userRoute);
app.listen(3000, () => {
console.log('Server is running on <http://localhost:3000>');
});
Conclusion
This structure is a good fit for the following development styles:
- Teams where multiple people or features are developed in parallel.
- Projects that want to start small and add features incrementally.
- Workplaces that want a design that separates responsibilities by feature, making it easy to change and maintain.
A structure isn’t something you decide on once and are done with; it’s something you keep iterating on.
You should continue reviewing your structure as the team grows and the project changes.