Goal: Keep controllers thin (HTTP → validation → call service) and put business logic & DB interaction in services and repositories. This makes code testable, maintainable, and production-ready.

Below I’ll give:

  1. Folder structure + responsibilities
  2. A compact TypeScript example (you already know TS — it helps with types) using Sequelize
  3. How to test & why this design matters

1) Recommended folder structure

src/
  db/
    index.ts           // sequelize instance, pooling config
  models/
    user.model.ts
    post.model.ts
  repositories/
    user.repository.ts // raw DB CRUD, single responsibility
  services/
    user.service.ts    // business logic, transactions, orchestration
  controllers/
    user.controller.ts // thin layer: validation -> service
  routes/
    user.routes.ts
  utils/
    errors.ts          // custom error classes
    logger.ts
  app.ts

2) Why the split?

This prevents controllers from becoming a dumping ground of SQL and business rules.


3) Example (TypeScript, production-minded)

src/db/index.ts

import { Sequelize } from "sequelize";
import "dotenv/config";

export const sequelize = new Sequelize({
  database: process.env.DB_DATABASE!,
  username: process.env.DB_USERNAME!,
  password: process.env.DB_PASSWORD!,
  host: process.env.DB_HOST,
  dialect: "postgres",
  pool: {
    max: 20,
    min: 2,
    acquire: 30000,
    idle: 10000,
  },
  logging: (sql, ms) => {
    // Use your logger instead of console.log in prod
    // log sql and execution time (ms)
    // logger.debug(sql, { ms });
  },
});

export const connectDB = async () => {
  try {
    await sequelize.authenticate();
    // Do NOT call sync({ alter: true }) in prod — use migrations.
    console.log("DB connected");
  } catch (err) {
    console.error("DB connection failed", err);
    process.exit(1);
  }
};

src/models/user.model.ts

import { DataTypes, Model, Optional } from "sequelize";
import { sequelize } from "../db";

interface UserAttributes {
  id: string;
  name: string;
  email: string;
  password: string;
  createdAt?: Date;
  updatedAt?: Date;
}
interface UserCreationAttrs extends Optional<UserAttributes, "id"> {}

export class User extends Model<UserAttributes, UserCreationAttrs> implements UserAttributes {
  public id!: string;
  public name!: string;
  public email!: string;
  public password!: string;
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;
}

User.init({
  id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
  name: { type: DataTypes.STRING(100), allowNull: false },
  email: { type: DataTypes.STRING(100), allowNull: false, unique: true },
  password: { type: DataTypes.STRING(100), allowNull: false },
}, {
  sequelize,
  tableName: "users",
  timestamps: true,
  underscored: true,
});