Skip to content

s1rserg/noted-backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

8 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ” Express Server App β€” Users & Tasks

Node TypeScript Express TypeORM PostgreSQL Zod Docker ESLint

A simple demonstration backend app that consists of:

  • πŸ”‘ Authentication (Auth)
  • πŸ§‘β€πŸš€ Users
  • βœ… Tasks

The project is written in TypeScript, uses Express 5, TypeORM, PostgreSQL, Zod for validation, and follows a clean modular structure with Dependency Injection via module composers.


πŸš€ Quick Start

Prerequisites:

  • Node.js v22.19.0
  • Docker + Docker Compose

Steps:

  1. Create or request a .env file with required variables (see env.example)
  2. Start infrastructure in Docker: docker compose up
  3. Install dependencies: npm i
  4. Run the app:
    • Dev: npm run start:dev
    • Prod: npm run start:prod

Useful scripts:

  • Build: npm run build
  • Lint: npm run lint / npm run lint:fix

Default ports:

  • App: APP_PORT=5001
  • Postgres: exposed on host as DB_PORT from .env (mapped to container 5432)

🧰 Tech Stack

  • 🟒 Node.js 22.19.0
  • ⚑ Express 5
  • πŸ—ƒοΈ TypeORM
  • 🐘 PostgreSQL
  • 🧩 Zod (validation)
  • πŸ” JWT (auth)
  • πŸ§ͺ TypeScript with strict mode and verbatimModuleSyntax
  • 🧰 ESLint + Prettier config
  • 🐳 Docker Compose for Postgres

🧭 Project Structure

Top-level directories you’ll interact with most:

  • app β€” application bootstrap and global composition (things not reused by modules)
  • infrastructure β€” infrastructural services (ConfigService, Database, Logger, etc.)
  • shared β€” reusable building blocks (types, utils, middlewares, schemas)
  • modules β€” main feature modules where most business logic lives
  • ambient β€” ambient type augmentations (e.g., Express Request extension)

Core feature modules:

  • auth
  • user
  • task

🧱 Architectural Notes

  • Repository pattern: all ORM/database queries are isolated inside Repository classes. Only repositories talk to TypeORM.
  • Services expose business logic and use repositories to access the database.
  • Controllers interact with services.
  • Validation is done with Zod via middlewares. Controllers receive already validated and typed data.
  • Dependency Injection is realized via per-module composers + a global composer.

πŸ§ͺ Validation & Typing

  • Validation: Zod + middlewares.
  • If there is a Zod schema, the TypeScript type must be inferred from it via z.infer.
  • If no Zod schema exists, use regular TypeScript types.

Example:

import { z } from 'zod';

export const SignInSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export type SignInDto = z.infer<typeof SignInSchema>;

πŸ”Ž Request Typing and Extended Express.Request

The Express Request type is augmented globally (see src/ambient/express.d.ts) with:

  • user?: ActiveUser β€” current user injected by auth middleware
  • validated?: unknown β€” storage for validated, transformed, typed data

Controllers must use the custom TypedRequest instead of express.Request to get strong typing for validated:

import type { Response } from 'express';
import type { TypedRequest } from '@types';

export class TaskController {
  findOne = async (
    req: TypedRequest<{ params: { id: number } }>,
    res: Response,
  ) => {
    const user = req.user!;
    const taskId = req.validated.params.id;
    // ...
    res.status(200).json({});
  };
}

🧩 Imports, Barrels and Paths

Rules for imports:

  1. Within a module β€” use only relative imports.
  2. Between modules β€” use absolute imports via tsconfig.paths aliases.
  3. Do not import deep internals of another module; import only from the module barrel (its index.ts).

Bad:

// ❌ Deep import from another module (forbidden)
import { UserEntity } from "@modules/user/entities/user.entity.js";

Good:

// βœ… Only from the module API (barrel)
import { UserEntity } from "@modules/user";

Barrel files (re-export index.ts):

  • Exist only at the root of a module to expose its public API.
  • Inside a module, do not re-export; import directly from files using relative paths.

Path aliases (see tsconfig.json):

  • @modules/*, @infrastructure/*, @app/*, @schemas/*, @utils/*, @ambient/*, @types.

🏷️ verbatimModuleSyntax rule (types-only imports)

The project enforces TypeScript’s verbatimModuleSyntax: true. If something is used only as a type β€” import it as a type.

Bad:

// ❌ Runtime import when only types are needed
import { ConfigService } from '@infrastructure/config-service';

Good:

// βœ… Type-only import
import type { ConfigService } from '@infrastructure/config-service';

You’ll see this pattern across the codebase (e.g., controllers and services use import type {...} where appropriate).


βš™οΈ Environment & ConfigService

Access to environment variables is centralized via ConfigService (src/infrastructure/config-service).

  • It validates .env using Zod (env.schema.ts) and exposes a typed env object.
  • When you change .env, you must update both validation (env.schema.ts) and typing (see types.ts/environment.d.ts in the same folder).

env variables (see env.example):

  • APP_PORT, APP_ENV
  • DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_DATABASE
  • JWT_SECRET, JWT_SALT_ROUNDS, ACCESS_TOKEN_TTL, REFRESH_TOKEN_TTL

Example usage:

const port = configService.env.APP_PORT; // number (coerced and validated)

πŸ§ͺ Dependency Injection & Composers

Each module has its own composer that:

  • Initializes and wires internal dependencies (repositories, services, controllers)
  • Builds the module router
  • Returns the router and other instances needed by other modules

There’s a global composer in app that:

  • Initializes infrastructural services (Logger, Config, Database)
  • Runs module composers
  • Returns composed module routers and cross-cutting instances (e.g., access token guard)

Example (simplified):

// app/composers/modules.composer.ts
const user = runUserModuleComposer({ dataSource });
const auth = runAuthModuleComposer({ dataSource, configService, userService: user.userService });
const task = runTaskModuleComposer({ dataSource });

return { moduleRouters: { userRouter: user.userRouter, authRouter: auth.authRouter, taskRouter: task.taskRouter }, accessTokenGuard: auth.accessTokenGuard };

Routing aggregation:

// app/composers/routers.composer.ts
rootRouter.use('/auth', moduleRouters.authRouter);
rootRouter.use('/users', [accessTokenGuard.canActivate], moduleRouters.userRouter);
rootRouter.use('/tasks', [accessTokenGuard.canActivate], moduleRouters.taskRouter);

🧱 Module Responsibilities

  • Repository: isolates TypeORM and database queries.
  • Service: uses repositories to implement business logic and DB access.
  • Controller: uses services; no direct ORM calls.

🐳 Docker

A PostgreSQL instance is provided via Docker Compose. The service reads credentials from your .env and exposes Postgres on ${DB_PORT}:5432.

services:
  postgres:
    image: postgres:17.5
    ports:
      - "${DB_PORT}:5432"
    environment:
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_DATABASE}
    env_file:
      - .env
    volumes:
      - postgres_data:/var/lib/postgresql/data

πŸ“œ License

ISC


πŸ™Œ Notes

  • This is a small demo application focused on structure and conventions.
  • Feel free to extend modules or add new ones following the same patterns.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors