All posts
ArchitectureMay 22, 2026·6 min read

Building Modular APIs in Node.js

File structure, barrel exports, dependency injection, and feature flags — practical patterns for keeping a growing Node.js API maintainable at scale.

The feature-first file structure

Group files by feature, not by type. Instead of controllers/, services/, models/, group everything about a feature together.

src/
  features/
    auth/
      auth.router.ts
      auth.service.ts
      auth.middleware.ts
      auth.types.ts
      auth.test.ts
    projects/
      projects.router.ts
      projects.service.ts
      projects.repo.ts
      projects.types.ts
      projects.test.ts
  lib/
    db.ts
    redis.ts
    logger.ts
  app.ts
  index.ts

Every file related to auth is in features/auth/. No hunting across three directories to understand a feature.

Barrel exports for clean imports

// features/auth/index.ts
export { authRouter } from './auth.router';
export { AuthService } from './auth.service';
export type { AuthUser } from './auth.types';

// In another file:
import { authRouter, AuthService } from '@/features/auth';

Dependency injection without a framework

You don't need InversifyJS. Constructor injection with TypeScript interfaces is usually enough:

export class ProjectService {
  constructor(
    private readonly repo: ProjectRepository,
    private readonly cache: CacheService,
    private readonly events: EventEmitter,
  ) {}

  async create(data: CreateProjectInput): Promise<Project> {
    const project = await this.repo.create(data);
    await this.cache.invalidate(`projects:user:${data.userId}`);
    this.events.emit('project.created', project);
    return project;
  }
}

Router composition

Each feature owns its router. The app assembles them with a prefix.

// app.ts
import { authRouter } from '@/features/auth';
import { projectsRouter } from '@/features/projects';

app.use('/auth', authRouter);
app.use('/projects', requireAuth, projectsRouter);

Feature flags

For gradual rollouts, implement a simple flag system keyed by user or percentage rollout before reaching for a paid tool:

const flags = {
  newDashboard: (user: User) => user.betaAccess,
  fastSearch: (user: User) => hashUser(user.id) % 100 < 20, // 20% rollout
};

app.get('/dashboard', (req, res) => {
  if (flags.newDashboard(req.user)) {
    return renderNewDashboard(req, res);
  }
  return renderOldDashboard(req, res);
});

Versioning your API

Prefix routes with /v1 from day one. When you need to make a breaking change, add /v2 alongside. Deprecate v1 with a sunset header. Never break existing clients.

app.use('/v1', v1Router);
app.use('/v2', v2Router);

Ready to put this into practice?

Deploy your Node.js app to production in minutes — zero YAML, automatic CI/CD, and HTTPS included.