Skip to content
advanced Phase 8 · TypeScript Project

Implementation & Deployment

Build and deploy the TypeScript project with CI/CD pipeline.

2h
0 problems
Topic Progress 0%

Project Setup

Project Setup and Configuration

Initialize Project

mkdir ts-api-project && cd ts-api-project
npm init -y
npm pkg set type="module"
npm pkg set scripts.dev="tsx watch src/index.ts"
npm pkg set scripts.build="tsc"
npm pkg set scripts.start="node dist/index.js"

npm install --save express zod dotenv jsonwebtoken bcryptjs
npm install --save-dev typescript tsx @types/node @types/express \
  @types/jsonwebtoken @types/bcryptjs vitest eslint prettier

Environment Setup

// src/config/env.ts
import 'dotenv/config';

interface Env {
  NODE_ENV: 'development' | 'production' | 'test';
  PORT: number;
  DATABASE_URL: string;
  JWT_SECRET: string;
  JWT_EXPIRES_IN: string;
}

function getEnv(): Env {
  return {
    NODE_ENV: (process.env.NODE_ENV as Env['NODE_ENV']) ?? 'development',
    PORT: parseInt(process.env.PORT ?? '3000', 10),
    DATABASE_URL: process.env.DATABASE_URL!,
    JWT_SECRET: process.env.JWT_SECRET!,
    JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '24h'
  };
}

export const env = getEnv();

Database Setup

// src/config/database.ts
import { env } from './env.js';

interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
  ssl: boolean;
}

export const dbConfig: DatabaseConfig = {
  host: new URL(env.DATABASE_URL).hostname,
  port: parseInt(new URL(env.DATABASE_URL).port || '5432'),
  database: new URL(env.DATABASE_URL).pathname.slice(1),
  ssl: env.NODE_ENV === 'production'
};

Type Definitions

Type Definitions

Models and DTOs

// src/types/models.ts
export interface User {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
  role: UserRole;
  createdAt: Date;
  updatedAt: Date;
}

export type UserRole = 'admin' | 'user';

export interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
  published: boolean;
  createdAt: Date;
  updatedAt: Date;
}

// src/types/dto.ts
export interface CreateUserDTO {
  name: string;
  email: string;
  password: string;
  role?: UserRole;
}

export interface LoginDTO {
  email: string;
  password: string;
}

export interface UpdatePostDTO {
  title?: string;
  content?: string;
  published?: boolean;
}

API Types

// src/types/api.ts
import { User } from './models.js';

export interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
  details?: Record<string, string[]>;
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  meta: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

// Type-safe authenticated request
import { Request } from 'express';

export interface AuthenticatedRequest extends Request {
  user?: {
    id: string;
    role: UserRole;
  };
}

Repository Layer

Repository Layer

Base Repository

// src/repositories/base-repository.ts
export interface BaseEntity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface BaseRepository<T extends BaseEntity> {
  findById(id: string): Promise<T | null>;
  findAll(options: FindOptions): Promise<T[]>;
  create(data: Omit<T, keyof BaseEntity>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T | null>;
  delete(id: string): Promise<boolean>;
  count(): Promise<number>;
}

export interface FindOptions {
  page?: number;
  limit?: number;
  sort?: string;
  order?: 'asc' | 'desc';
}

User Repository

// src/repositories/user-repository.ts
import { User, CreateUserDTO } from '../types/index.js';
import { BaseRepository, FindOptions } from './base-repository.js';
import { db } from '../config/database.js';

export class UserRepository implements BaseRepository<User> {
  async findById(id: string): Promise<User | null> {
    const result = await db.query(
      'SELECT * FROM users WHERE id = $1',
      [id]
    );
    return result.rows[0] ?? null;
  }

  async findAll(options: FindOptions): Promise<User[]> {
    const { page = 1, limit = 10, sort = 'createdAt', order = 'desc' } = options;
    const offset = (page - 1) * limit;

    const result = await db.query(
      `SELECT * FROM users ORDER BY ${sort} ${order} LIMIT $1 OFFSET $2`,
      [limit, offset]
    );
    return result.rows;
  }

  async create(data: Omit<User, keyof BaseEntity>): Promise<User> {
    const result = await db.query(
      `INSERT INTO users (name, email, password_hash, role)
       VALUES ($1, $2, $3, $4) RETURNING *`,
      [data.name, data.email, data.passwordHash, data.role]
    );
    return result.rows[0];
  }

  async findByEmail(email: string): Promise<User | null> {
    const result = await db.query(
      'SELECT * FROM users WHERE email = $1',
      [email]
    );
    return result.rows[0] ?? null;
  }
}

Service and Controller Layers

Service and Controller Layers

User Service

// src/services/user-service.ts
import { UserRepository } from '../repositories/user-repository.js';
import { CreateUserDTO, User } from '../types/index.js';
import { hashPassword, comparePassword } from '../utils/password.js';
import { AppError } from '../utils/errors.js';

export class UserService {
  constructor(private repo: UserRepository) {}

  async create(dto: CreateUserDTO): Promise<Omit<User, 'passwordHash'>> {
    const existing = await this.repo.findByEmail(dto.email);
    if (existing) {
      throw new AppError('EMAIL_EXISTS', 'Email already registered', 409);
    }

    const passwordHash = await hashPassword(dto.password);
    const user = await this.repo.create({
      ...dto,
      passwordHash,
      role: dto.role ?? 'user'
    } as any);

    const { passwordHash: _, ...userWithoutPassword } = user;
    return userWithoutPassword;
  }

  async authenticate(email: string, password: string): Promise<string> {
    const user = await this.repo.findByEmail(email);
    if (!user) {
      throw new AppError('INVALID_CREDENTIALS', 'Invalid email or password', 401);
    }

    const valid = await comparePassword(password, user.passwordHash);
    if (!valid) {
      throw new AppError('INVALID_CREDENTIALS', 'Invalid email or password', 401);
    }

    return generateToken({ id: user.id, role: user.role });
  }
}

User Controller

// src/controllers/user-controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user-service.js';
import { AuthenticatedRequest, ApiResponse } from '../types/index.js';
import { CreateUserDTO } from '../types/index.js';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  password: z.string().min(8),
  role: z.enum(['admin', 'user']).optional()
});

export class UserController {
  constructor(private service: UserService) {}

  create = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const dto = CreateUserSchema.parse(req.body);
      const user = await this.service.create(dto);
      res.status(201).json({ success: true, data: user } as ApiResponse<typeof user>);
    } catch (error) {
      next(error);
    }
  };

  login = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const { email, password } = req.body;
      const token = await this.service.authenticate(email, password);
      res.json({ success: true, data: { token } });
    } catch (error) {
      next(error);
    }
  };

  getProfile = async (req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> => {
    try {
      const user = await this.service.findById(req.user!.id);
      if (!user) {
        throw new AppError('NOT_FOUND', 'User not found', 404);
      }
      const { passwordHash: _, ...userWithoutPassword } = user;
      res.json({ success: true, data: userWithoutPassword });
    } catch (error) {
      next(error);
    }
  };
}