Best Practices for Structuring your Projects – 4. Hybrid Structure
This blog post is a translation of a Japanese article posted on October 16th, 2025.
Introduction
In application development, directory structure is a critical design element that directly impacts maintainability, scalability, and development efficiency.
This article introduces a Hybrid Structure for teams grappling with how best to organize their project directories.
⚠️ 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 Strategy
- Organize directories based on both “features” and “roles” (layers).
- Group components, logic, and state by feature, while organizing shared processing as layers.
- Design flexibly according to app scale or domain complexity—for example, incorporating a layered structure within features or extracting logic to a shared area.
- Centralize cross-cutting concerns in
shared/orcore/to maintain overall clarity while separating responsibilities.
Advantages
- Combines the simplicity of a layer-based approach with the scalability of a feature-based one. It achieves both organization by role and separation by feature unit.
- Enables appropriate splitting for different development teams, even in large-scale projects. It facilitates parallel development and allows management by team.
- High code reusability and excellent maintainability. It properly splits common logic, improving development efficiency.
Disadvantages
- It takes time to find the optimal balance. Decisions at the design stage are crucial, and determining the right way to split things takes time.
- The structure needs review as the project grows. If the initial design is poor, the structure might break down as the project expands.
Criteria for Splitting Layers vs. Features
Deciding “how far to isolate a feature” versus “how much to commonize as a layer” significantly affects the maintainability and scalability of the entire architecture.
Please refer to separate directory structure articles for specific criteria on each approach:
- Best Practices for Structuring your Projects – 2. Feature Based Structure
- Best Practices for Structuring your Projects – 3. Layer Based Structure
💡 While basically aiming for a “structure complete within a single feature,”
we recommend separating logic that can be shared into layers to enhance flexibility and reusability.
Directory Structure Examples
Frontend
src/
├── core/ // Foundation processing for the entire app (Layer structure)
│ ├── api/ // API client settings
│ │ └── axiosClient.ts
│ ├── auth/ // Authentication processing
│ │ ├── authProvider.ts
│ │ └── token.ts
│ ├── config/ // Environment configuration
│ │ └── env.ts
│ ├── error/ // Common error handling
│ │ └── errorHandler.ts
│ ├── store/ // Initialization/Persistence of global state
│ │ └── globalStore.ts
│ └── index.ts // Entry point for initialization
│
├── shared/ // Shared reusable parts (Layer structure)
│ ├── components/ // Common UI (Button, Modal, etc.)
│ │ ├── Button.tsx
│ │ └── Modal.tsx
│ ├── hooks/ // Generic hooks (useDebounce, etc.)
│ │ └── useDebounce.ts
│ ├── utils/ // Pure functions
│ │ └── formatDate.ts
│ ├── constants/ // Constants
│ │ └── app.ts
│ └── types/ // Common type definitions
│ └── index.ts
│
├── features/ // Separated by feature (Internal layer structure)
│ ├── user/
│ │ ├── components/ // UI specific to this feature
│ │ ├── hooks/ // State acquisition / Side effects
│ │ ├── usecases/ // Application logic
│ │ ├── repositories/ // API or local data fetching
│ │ ├── store/ // State management (Zustand, etc.)
│ │ └── index.ts // Exports
│ ├── product/
│ └── order/
│
├── pages/ // Screen UIs combining multiple features
│ ├── user.tsx
│ └── dashboard.tsx
│
└── main.tsx // Application entry point
💡 Key Design Points
→ features/: Each feature is self-contained, holding its own UI, state, and logic.
→ core/: Handles global initialization, configuration, and API client management.
→ shared/: Targets fully cross-cutting reusable elements (Common UI, hooks, utils, etc.).
→ pages/: Constructs screens by combining features.
→ Using a layered structure divided by responsibility inside core/, shared/, and features/ prevents structural breakdown and improves maintainability.
Example: Assembling the UI
// 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} />;
};
// 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/
├── core/ // Foundation functions for the entire app (Layer structure)
│ ├── config/ // Environment config / Constants
│ │ └── env.ts
│ ├── database/ // DB connection / Initialization
│ │ └── connection.ts
│ ├── logger/ // Logging
│ │ └── logger.ts
│ ├── auth/ // Auth / JWT related
│ │ └── verifyToken.ts
│ ├── error/ // Common error handling
│ │ └── errorHandler.ts
│ └── index.ts // For app initialization
│
├── shared/ // Shared parts (Types, Utilities)
│ ├── utils/ // Generic functions
│ │ └── date.ts
│ ├── constants/ // Constants
│ │ └── roles.ts
│ └── types/ // Type definitions
│ └── user.ts
│
├── features/ // Configuration by feature (Internal layer structure)
│ ├── user/
│ │ ├── usecases/ // Business logic
│ │ ├── repositories/ // DB operations / External API communication
│ │ ├── models/ // ORM models
│ │ ├── schema/ // Validation schemas
│ │ └── index.ts // Exports
│ ├── product/
│ └── order/
│
├── routers/ // API Routing
│ ├── user/route.ts
│ ├── product/route.ts
│ └── dashboard/route.ts
│
├── main.ts // Server startup / Routing integration
└── tsconfig.json
💡 Key Design Points
→ features/*: Each feature is self-contained, encapsulating usecases, repositories, schemas, etc.
→ routers/: Call usecases directly; a separate controller layer is unnecessary.
→ core/: Centralize common initialization, authentication, and DB connections.
→ shared/: Group types, constants, and pure functions here for easy reuse.
→ Ensuring core/, shared/, and features/ all possess an internal layered structure clarifies roles and makes the structure robust.
Example: Assembling the API
// routers/user/route.ts
import { Router } from 'express';
import { getUserById } from '@/features/user/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 '@/features/user/usecases/userUsecase';
import { getOrdersByUserId } from '@/features/order/usecases/orderUsecase';
import { getProductsByIds } from '@/features/product/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;
Example: Combining Routes
// routers/dashboard/route.ts
import { Router } from 'express';
import { getUserById } from '@/features/user/usecases/userUsecase';
import { getOrdersByUserId } from '@/features/order/usecases/orderUsecase';
import { getProductsByIds } from '@/features/product/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;
Conclusion
The Hybrid Structure is a design that balances layer clarity with maintainability and scalability at the feature level.
It is suitable for cases such as:
- Medium-to-large projects where multiple members develop features or responsibilities in parallel.
- You want to achieve separation of both features and responsibilities simultaneously.
- You want to add features flexibly while maintaining high scalability and reusability.
A structure isn’t something you decide 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.