# BEKIND Backend — Coding Conventions

Author: Nguyen Thi Nguyet Que Created on: May 2, 2026

**Stack:** TypeScript · Node.js · Express.js · Sequelize · PostgreSQL · Redis **Pattern:** Layered
Architecture (Controller → Service → Provider → Model)

---

## 1. Architecture Layers

| Layer      | Folder             | Responsibility                                                                 |
| ---------- | ------------------ | ------------------------------------------------------------------------------ |
| Controller | `src/controllers/` | HTTP handling, OpenAPI docs, middleware chaining. Thin — delegate to Services. |
| Service    | `src/services/`    | Business logic, orchestration. Stateless `static` methods preferred.           |
| Provider   | `src/providers/`   | Data access over Sequelize. One per model, extends `BaseProvider`.             |
| Model      | `src/models/`      | Sequelize definitions. Auto-generated via `sequelize-auto`.                    |
| Middleware | `src/middlewares/` | Auth, validation, rate limit, response formatting.                             |
| Interface  | `src/interfaces/`  | Shared TypeScript types and DTOs.                                              |
| Template   | `src/templates/`   | Base classes, email templates, Swagger schemas. No business logic.             |
| Utils      | `src/utils/`       | Pure helpers. No external service calls.                                       |
| Constants  | `src/constants/`   | Enums, config objects, magic values.                                           |

**Request Flow:** HTTP → Middleware → Controller → Service → Provider → Model → PostgreSQL

---

## 2. Naming Conventions

### Files & Folders

| Category         | Convention               | Example                                |
| ---------------- | ------------------------ | -------------------------------------- |
| Controllers      | camelCase.ts             | `web.ts`, `sendOtp.ts`                 |
| Route folders    | kebab-case               | `forgot-password/`, `role-permission/` |
| Services         | camelCase.ts             | `sessionService.ts`                    |
| Service classes  | PascalCase + Service     | `SessionService`                       |
| Providers        | PascalCase + Provider.ts | `UserProvider.ts`                      |
| Provider classes | PascalCase + Provider    | `UserProvider`                         |
| Models           | PascalCase.ts            | `User.ts`, `UserSession.ts`            |
| Middleware       | camelCase.ts             | `rateLimiter.ts`                       |
| Interfaces       | I + PascalCase.ts        | `IApi.ts`, `IProvider.ts`              |
| Route params     | `{id}.ts`                | `user/{id}/reset-password.ts`          |

### Database

| Category   | Convention                               | Example                      |
| ---------- | ---------------------------------------- | ---------------------------- |
| Tables     | snake_case, plural                       | `users`, `user_sessions`     |
| Columns    | snake_case                               | `first_name`, `created_at`   |
| PK         | `id` (UUID/string)                       | `id`                         |
| FK         | `{table}_id`                             | `user_id`, `role_id`         |
| Timestamps | `created_at`, `updated_at`, `deleted_at` | soft-delete via `deleted_at` |

### Code

- Variables/params: `camelCase` (`userId`, `deviceInfo`)
- Booleans: prefix `is`, `has`, `should` (`isActive`, `hasPermission`)
- Constants: `UPPER_SNAKE_CASE` (`MAX_RETRY`, `TOKEN_LIFETIME`)
- Private fields: `camelCase` (no `_` prefix)
- Async functions: always use `async` keyword

---

## 3. Directory Structure

```
src/
  constants/
  controllers/api/v1/
    auth/
    file/
    notifications/
    permission/
    role/
    role-permission/
    user/
    user-role/
  interfaces/
    error/
      handler/
      violations/
  middlewares/
    mod/
    validators/
  models/
  providers/
  services/auth/
  services/data/
  services/database/sequelize/
  services/file-system/
  services/notification/
    helpers/
  services/scheduler/
    jobs/
  services/storage/
  templates/base/
  templates/email/
  templates/swagger/
  utils/
```

---

## 4. Code Patterns

### 4.1 Controller

```typescript
import { Application } from 'express';
import { Resource } from 'express-automatic-routes';
import { Req, Res } from '#interfaces/IApi';
import type { LoginResponse } from '#contracts/auth.schema';

export default (_express: Application) => {
	return <Resource>{
		post: {
			middleware: [validateLogin] as any,
			handler: async (req: Req, res: Res) => {
				try {
					const result = await AuthService.login(req.body);
					const payload = {
						success: true,
						message: 'Thành công',
						message_en: 'Success',
						trace_id: req.id,
						timeStamp: new Date().toISOString(),
						violations: null,
						responseData: result,
					} satisfies LoginResponse;
					return res.json(payload);
				} catch (error) {
					return res.error(error);
				}
			},
		},
	};
};
```

- Export default factory returning `Resource`
- OpenAPI docs: Add Zod contracts in `src/contracts/<domain>.paths.ts`
- Use `satisfies` keyword for type-safe response building
- Middleware in `middleware` array
- Use `res.sendOk()` / `res.error()` — never raw `res.json()` (except with `satisfies`)
- Always `try/catch` in handler

### 4.2 Service

```typescript
export class SessionService {
	static async createSession(data: SessionData): Promise<UserSession> {
		// business logic only
	}
}
```

- Prefer `static` methods for stateless services
- Call Providers, not Models directly
- Throw domain errors; never handle HTTP status codes

### 4.3 Provider

```typescript
import { BaseProvider } from '#templates/base/provider';
import { User } from '#models/User';

export class UserProvider extends BaseProvider<User> {
	public static instance: UserProvider;

	public static getInstance(): UserProvider {
		UserProvider.instance ??= new UserProvider();
		return UserProvider.instance;
	}

	public static get model() {
		return User;
	}

	constructor() {
		super('User');
	}
}
```

- Must be singleton (`getInstance()`)
- Expose `static get model()` for generator compatibility
- Constructor calls `super('ModelName')`

### 4.4 Model

- Auto-generate via `npm run gen-db` / `npm run gen-providers`
- Manual edits allowed for associations and custom methods
- Use `declare` for attributes in class body

---

## 5. Path Aliases

Use tsconfig paths — no relative imports beyond sibling files.

| Alias            | Maps To             |
| ---------------- | ------------------- |
| `#controllers/*` | `src/controllers/*` |
| `#services/*`    | `src/services/*`    |
| `#providers/*`   | `src/providers/*`   |
| `#models/*`      | `src/models/*`      |
| `#middlewares/*` | `src/middlewares/*` |
| `#interfaces/*`  | `src/interfaces/*`  |
| `#constants/*`   | `src/constants/*`   |
| `#templates/*`   | `src/templates/*`   |
| `#utils/*`       | `src/utils/*`       |
| `#/*`            | `src/*`             |

---

## 6. Response Format

```json
{
	"message": "Thành công",
	"message_en": "Success",
	"responseData": {},
	"violations": []
}
```

| Method                                                      | Usage                      |
| ----------------------------------------------------------- | -------------------------- |
| `res.sendOk({ data, message, message_en, statusCode })`     | Success                    |
| `res.error(err)`                                            | Automatic error processing |
| `res.sendErrorStatus({ status, message, message_en, err })` | Manual error with status   |
| `res.sendError({ err })`                                    | Server error (500)         |

---

## 7. Error Handling

```typescript
throw new MeUError(404, 'DB', `Record not found`);
throw new MeUError(409, 'DB', { duplicateFields });
```

- `errorCode`: numeric (404, 409, -999)
- `errorType`: `'API'` | `'DB'`
- `errorData`: optional context object

---

## 8. Authentication

| Platform       | Token Storage   | Header                     |
| -------------- | --------------- | -------------------------- |
| `web`          | HttpOnly Cookie | —                          |
| `mobile`       | Response Body   | `X-Platform: mobile`       |
| `zalo_miniapp` | Response Body   | `X-Platform: zalo_miniapp` |

- Web: tokens in HttpOnly cookies
- Mobile/Zalo: tokens in response body
- Access token: 15 min. Refresh token: 7 days.
- Refresh token rotation with reuse detection

---

## 9. Generator Scripts

| Script                  | Purpose                          |
| ----------------------- | -------------------------------- |
| `npm run gen-api`       | CRUD controllers from models     |
| `npm run gen-api-bulk`  | Bulk generate APIs               |
| `npm run gen-db`        | Models + providers from database |
| `npm run gen-providers` | Providers only                   |
| `npm run gen-routes`    | Regenerate route index           |

---

## 10. Testing & Quality

| Script                     | Purpose                      |
| -------------------------- | ---------------------------- |
| `npm run test:unit`        | Unit tests                   |
| `npm run test:integration` | Integration tests            |
| `npm run test:coverage`    | Coverage report              |
| `npm run lint`             | ESLint check                 |
| `npm run format`           | Prettier format              |
| `npm run type-check`       | TypeScript check             |
| `npm run quality:check`    | Lint + type-check + coverage |

---

## 11. Commits

Conventional Commits via `@commitlint/config-conventional`:

```
feat(auth): add Zalo mini app login
fix(session): handle expired refresh token race condition
docs(api): update swagger spec
refactor(provider): extract cache helper
test(auth): add refresh token rotation tests
```

---

## 12. Security

- Never return tokens in body for `web` platform
- Use `res.error()` — never leak stack traces
- Hash tokens before logging (`TokenService.hashToken`)
- Encrypt tokens at rest (DB `encrypt_token`/`decrypt_token`)
- Rate-limit auth endpoints (`createRateLimit`)
- Validate platform header for mobile/zalo
- Parameterized queries only — no raw string concatenation
- CORS `credentials: true` with origin whitelist in production

---
