Best Practices for Structuring your Projects – 3. Layer Based Structure
This blog post is a translation of a Japanese article posted on September 26th, 2025.
Introduction
In application development, directory structure is a design element that directly impacts maintainability, scalability, and development efficiency.
This article introduces a simple and intuitive layer-based structure for teams struggling with the following kinds of issues.
⚠️ 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 role (layer).
- Clearly separate the responsibilities of each layer (e.g.,
controllers/,services/,models/) - Ensure readability and flexibility by building the business logic flow hierarchically.
Advantages
- Simple and intuitive: It’s easy for new participants to learn because each layer is clearly separated.
- Easy to separate code by responsibility: the dependencies between modules are organized, making it easy to limit the scope of impact when making changes.
- Well-suited for small to medium-sized projects: for simple projects, a layer-based structure is easy to manage.
Disadvantages
- Folder structure can easily become bloated in large-scale projects: as features increase, managing each layer becomes complicated.
- Dependencies between features can become complex: If processes that cross multiple layers increase, the scope of impact during changes can widen.
Criteria for Separating Layers
“How to separate layers” significantly impacts the overall clarity and maintainability of the structure. Let’s design based on criteria like the following:
| Classification Criterion | Details |
|---|---|
| Clarity of Responsibility | Separate different concerns, such as display, state manipulation, business logic, and data access. |
| Difference in Change Frequency | Separate layers like components/ (frequent UI changes) and usecases/ (logic-centric, stable) to limit the scope of impact. |
| Reusability | Make processes that are easy to share (hooks, utils, middleware, etc.) independent layers. |
| Testing Units | Carve out layers at a granularity that is easy to unit test (usecase, repository, etc.). |
| Development Setup / Skill Separation | Also effective when dividing work between UI-centric development and domain-logic-centric development. |
💡 The basic goal is to hierarchically separate UI, logic, and data retrieval.
For parts where it’s difficult to draw a clear boundary, tightly couple them first and then gradually separate them as the scale or responsibility increases.
For example, you might start by writing the UI, logic, and data retrieval code in pages/user.tsx. Later, you could create components/UserList.tsx to separate the user list UI, and then create repository/userRepository.ts and write the data retrieval processing there.
Example Directory Structure
Frontend
src/
├── core/ // Core foundation for the entire app
├── pages/ // Page-level composition (UI)
│ ├── dashboard.tsx
│ └── user.tsx
├── components/ // UI parts (uses usecases and hooks)
│ ├── UserList.tsx
│ └── DashboardCard.tsx
├── hooks/ // State manipulation, side-effect abstraction
│ ├── useUser.ts
│ └── useProduct.ts
├── usecases/ // Application logic
│ ├── userUsecase.ts
│ └── productUsecase.ts
├── repository/ // Data retrieval processing (API access, etc.)
│ ├── userRepository.ts
│ └── productRepository.ts
├── store/ // State management like Zustand or Redux
│ └── userStore.ts
├── utils/ // General-purpose functions
│ └── format.ts
└── tsconfig.json
- Place all UI components in
components/, which handle the responsibility of display only. - Separate state manipulation and side-effect logic into
hooks/to decouple them from the UI and increase reusability. - Consolidate application operation units (use cases) in
usecases/, and handle the UI-specific combinations inpages/.
💡 Key Design Points
→ By clearly separating single responsibilities for each layer, each layer becomes easier to change and test.
→ In pages/, construct the UI by freely combining hooks, usecases, and components.
→ It’s easier to maintain loose coupling if you call state management and data retrieval processes together via a usecase.
→ core/ handles app-wide initialization, configuration, and common API client processing.
UI Assembly Example
// pages/user.tsx
import { UserList } from '@/components/UserList';
import { useUser } from '@/hooks/useUser';
export const UserPage = () => {
const { users } = useUser();
return <UserList users={users} />;
};
// pages/dashboard.tsx
import { UserCard } from '@/components/UserCard';
import { ProductCard } from '@/components/ProductCard';
import { useUser } from '@/hooks/useUser';
import { useProduct } from '@/hooks/useProduct';
export const Dashboard = () => {
const { currentUser } = useUser();
const { featuredProduct } = useProduct();
return (
<>
<UserCard name={currentUser.name} />
<ProductCard product={featuredProduct} />
</>
);
};
Backend
src/
├── routers/ // Routing definitions per endpoint
│ ├── user/route.ts
│ └── dashboard/route.ts
├── usecases/ // Application logic
│ ├── userUsecase.ts
│ ├── orderUsecase.ts
│ └── productUsecase.ts
├── repository/ // Connection to DB or external APIs
│ ├── userRepository.ts
│ ├── orderRepository.ts
│ └── productRepository.ts
├── middleware/ // Common processing (e.g., auth, errorHandler)
│ └── errorHandler.ts
├── types/ // Common type definitions
│ └── user.ts
├── utils/ // Common functions
│ └── logger.ts
└── tsconfig.json
- Place each endpoint in
routers/, which is responsible for handling HTTP requests/responses. - Implement application operation units (use cases) in
usecases/, with a structure where they are called from the router. - Consolidate persistence processing and interaction with external APIs in
repository/to separate them from domain logic. - Define common error handling and authentication in
middleware/to ensure separation of concerns and reusability.
💡 Key Design Points
→ Structure each layer to have clear responsibilities and a clear dependency direction (top-down).
→ Consolidate endpoint processing in routes/*/route.ts.
→ Design flexibly by combining return values so that the response structure can be configured freely.
→ Explicitly mount all routes in main.ts to centrally manage the app’s starting point.
API Assembly Example
// routers/user/route.ts
import { Router } from 'express';
import { getUserById } from '@/usecases/userUsecase';
const router = Router();
router.get('/:userId', async (req, res) => {
const user = await getUserById(req.params.userId);
res.json(user);
});
export default router;
// routers/dashboard/route.ts
import { Router } from 'express';
import { getUserById } from '@/usecases/userUsecase';
import { getOrdersByUserId } from '@/usecases/orderUsecase';
import { getProductsByIds } from '@/usecases/productUsecase';
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;
Routing Combination Example
// 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:
- Small teams or development setups that include less experienced members.
- Projects with complex domains where readability and organization of logic are important.
- An approach where you start small and add features incrementally.
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.