Best Practices for Structuring your Projects – 3. Layer Based Structure

Kodai Nakahara

2025.11.14

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 CriterionDetails
Clarity of ResponsibilitySeparate different concerns, such as display, state manipulation, business logic, and data access.
Difference in Change FrequencySeparate layers like components/ (frequent UI changes) and usecases/ (logic-centric, stable) to limit the scope of impact.
ReusabilityMake processes that are easy to share (hooks, utils, middleware, etc.) independent layers.
Testing UnitsCarve out layers at a granularity that is easy to unit test (usecase, repository, etc.).
Development Setup / Skill SeparationAlso 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 in pages/.

💡 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.

採用情報

Blog一覧へ戻る

Feel free to contact us

We provide support
for all aspects of SRE,
from design and technical support
to deployment of tools for SRE operations.

Brochure request and inquiries