UXDL Docs

Folder Structure

Standard Express layout — routes, controllers, services, and middleware.

Every Express service follows the same folder structure. Consistency makes it easy to navigate any backend repo.

Project layout

plaintext
backend-api/
├── src/
│   ├── server.ts              # Entry point — starts HTTP server
│   ├── app.ts                 # Express app factory (used in tests too)
│   │
│   ├── routes/                # Route definitions (thin — delegate to controllers)
│   │   ├── health.routes.ts
│   │   └── v1/
│   │       ├── index.ts       # Mounts all v1 routers
│   │       ├── users.routes.ts
│   │       └── projects.routes.ts
│   │
│   ├── controllers/           # Request/response handling
│   │   ├── users.controller.ts
│   │   └── projects.controller.ts
│   │
│   ├── services/              # Business logic (no Express types here)
│   │   ├── users.service.ts
│   │   ├── projects.service.ts
│   │   ├── push.service.ts    # FCM send helpers
│   │   ├── uploads.service.ts # S3 presigned URLs
│   │   └── email/             # Email provider adapters
│   │       ├── index.ts       # Factory (ses | sendgrid | brevo)
│   │       ├── ses.provider.ts
│   │       ├── sendgrid.provider.ts
│   │       └── brevo.provider.ts
│   │
│   ├── middleware/            # Express middleware
│   │   ├── auth.middleware.ts
│   │   ├── rate-limit.middleware.ts
│   │   ├── validate.middleware.ts
│   │   └── error.middleware.ts
│   │
│   ├── models/                # Sequelize / Mongoose models
│   │   └── user.model.ts
│   │
│   ├── db/                    # Database config & Drizzle schema
│   │   ├── index.ts           # Connection pool
│   │   ├── schema/            # Drizzle schema files
│   │   └── migrations/        # Drizzle / Sequelize migrations
│   │
│   ├── socket/                # Socket.io setup
│   │   ├── index.ts           # IO server init
│   │   ├── auth.middleware.ts # Socket auth
│   │   └── handlers/          # Event handlers per domain
│   │       └── notifications.handler.ts
│   │
│   ├── lib/                   # Shared utilities
│   │   ├── cognito.ts         # JWT verification
│   │   ├── supabase.ts        # Supabase client singleton
│   │   ├── s3.ts              # AWS S3 client
│   │   ├── fcm.ts             # Firebase Admin init
│   │   └── logger.ts
│   │
│   ├── types/                 # Shared TypeScript types & Express augmentations
│   │   ├── express.d.ts       # req.user augmentation
│   │   └── api.types.ts
│   │
│   └── validators/            # Zod schemas for request validation
│       ├── users.validator.ts
│       └── projects.validator.ts

├── tests/
│   ├── unit/
│   └── integration/

├── .env.example
├── docker-compose.yml
├── Dockerfile
├── tsconfig.json
└── package.json

Layer responsibilities

LayerResponsibilityMust NOT
routes/Define paths, attach middleware, call controllerContain business logic
middleware/Auth, validation, rate limiting, loggingCall database directly
controllers/Parse request, call service, format responseContain complex business rules
services/Business logic, orchestrationImport Express Request/Response
models/ · db/Data access, schema definitionsHandle HTTP concerns

Route example

typescript
// src/routes/v1/projects.routes.ts
import { Router } from "express";
import { authenticate } from "../../middleware/auth.middleware";
import { requireScope } from "../../middleware/scope.middleware";
import { validate } from "../../middleware/validate.middleware";
import { createProjectSchema } from "../../validators/projects.validator";
import * as projectsController from "../../controllers/projects.controller";
 
const router = Router();
 
router.get("/", authenticate, projectsController.list);
router.post(
  "/",
  authenticate,
  requireScope("write:projects"),
  validate(createProjectSchema),
  projectsController.create,
);
 
export default router;

Controller example

typescript
// src/controllers/projects.controller.ts
import { Request, Response, NextFunction } from "express";
import * as projectsService from "../services/projects.service";
 
export async function list(req: Request, res: Response, next: NextFunction) {
  try {
    const projects = await projectsService.listByUser(req.user!.sub);
    res.json({ data: projects });
  } catch (err) {
    next(err);
  }
}

Service example

typescript
// src/services/projects.service.ts
import { ProjectModel } from "../models/project.model";
 
export async function listByUser(userId: string) {
  return ProjectModel.findAll({ where: { ownerId: userId } });
}

Naming conventions

ItemConventionExample
Route files{resource}.routes.tsusers.routes.ts
Controllers{resource}.controller.tsusers.controller.ts
Services{resource}.service.tsusers.service.ts
Validators{resource}.validator.tsusers.validator.ts
Socket handlers{domain}.handler.tsnotifications.handler.ts