NODEJS Contents

Project Structure: Feature vs Layer Folders

Learn practical project structures for Node.js backends, how to avoid spaghetti imports, and how to organize code for scale, testing, and long-term maintenance.

On this page

Why Project Structure Matters

In backend projects, the biggest cost is not writing code, it is maintaining and changing it safely. A good structure makes the next 6 months easier. A bad structure makes every change risky.

Common Failure Mode

The typical failure pattern is “everything in one folder” or “random folders” that grow without boundaries. You end up with circular dependencies, duplicated logic, and untestable code.

Principle: Separate Concerns

  • App wiring: server startup, dependency wiring, routes registration
  • Modules/features: business capabilities (users, orders, billing)
  • Shared: cross-cutting utilities (logger, config, errors)
  • Infrastructure: DB clients, external services, adapters

Recommended Baseline Structure

src/
  index.ts
  app/
    createApp.ts
    routes.ts
  modules/
    users/
      users.controller.ts
      users.service.ts
      users.repo.ts
      users.dto.ts
    orders/
      orders.controller.ts
      orders.service.ts
      orders.repo.ts
      orders.dto.ts
  shared/
    config.ts
    logger.ts
    errors.ts
    http.ts
  infra/
    db.ts
    redis.ts
tests/

Feature-Based vs Layer-Based

Two popular styles exist:

  • Layer-based: controllers/, services/, repositories/ across the whole app
  • Feature-based: each feature owns its controller/service/repo

For growing backends, feature-based structure usually scales better because ownership and boundaries are clearer.

Rule: No Business Logic in Controllers

Controllers should translate HTTP to application calls. Business logic belongs in services/use-cases. This makes testing easier and avoids “HTTP coupled” business rules.

Rule: Keep Dependencies Pointing Inward

Higher-level modules should not depend on lower-level infrastructure details directly. Instead, define interfaces at the boundary and inject implementations.

Example: Minimal Wiring

// src/index.ts
import { createApp } from './app/createApp.js';

const app = createApp();
app.listen(3000, () => console.log('Listening on 3000'));

Example: Module Boundary

// src/modules/users/users.service.ts
export class UsersService {
  constructor(private repo: UsersRepo) {}

  async getUser(id: string) {
    return this.repo.findById(id);
  }
}

Testing Benefit

With clear boundaries, you can unit test services without running the HTTP server or database. Integration tests then validate the wiring.

Production Insight

  • Structure is an API for your team.
  • Good boundaries reduce merge conflicts and regressions.
  • Clear layering makes it easier to add caching, auth, logging later.

JS Note

Structure rules apply equally to JS and TS. TypeScript simply makes boundaries more explicit via types and interfaces.

Next Step

Next we will cover linting and formatting: how to keep code style consistent and prevent common mistakes automatically.