본문으로 건너뛰기 TypeScript REST API Project | Express· Layered Architecture

TypeScript REST API Project | Express· Layered Architecture

TypeScript REST API Project | Express· Layered Architecture

이 글의 핵심

TypeScript REST API tutorial: Express, tsconfig, DTOs (Omit/Partial), controller/service/database layers, ApiResponse JSON, and curl examples for user CRUD.

Introduction

Let’s build a type-safe REST API with TypeScript and Express.

1. Project setup

Initialize

mkdir typescript-api
cd typescript-api
npm init -y
npm install express
npm install --save-dev typescript @types/node @types/express ts-node nodemon

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": [ES2020],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": [src/**/*],
  "exclude": ["node_modules", "dist"]
}

package.json scripts

{
  "scripts": {
    "dev": "nodemon --exec ts-node src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

2. Type definitions

src/types/user.ts

export interface User {
    id: string;
    name: string;
    email: string;
    age: number;
    createdAt: Date;
}
export type CreateUserDto = Omit<User, "id" | "createdAt">;
export type UpdateUserDto = Partial<CreateUserDto>;
export type UserResponse = Omit<User, "createdAt"> & {
    createdAt: string;
};

src/types/api.ts

export interface ApiResponse<T> {
    success: boolean;
    data?: T;
    error?: string;
}
export interface PaginatedResponse<T> {
    items: T[];
    total: number;
    page: number;
    pageSize: number;
}

3. In-memory database

src/database/users.ts

import { User } from "../types/user";
class UserDatabase {
    private users: Map<string, User> = new Map();
    private currentId = 1;
    
    create(data: Omit<User, "id" | "createdAt">): User {
        const user: User = {
            id: `U${String(this.currentId++).padStart(3, "0")}`,
            ...data,
            createdAt: new Date()
        };
        this.users.set(user.id, user);
        return user;
    }
    
    findAll(): User[] {
        return Array.from(this.users.values());
    }
    
    findById(id: string): User | undefined {
        return this.users.get(id);
    }
    
    update(id: string, data: Partial<User>): User | undefined {
        const user = this.users.get(id);
        if (!user) return undefined;
        
        const updated = { ...user, ...data };
        this.users.set(id, updated);
        return updated;
    }
    
    delete(id: string): boolean {
        return this.users.delete(id);
    }
}
export const userDb = new UserDatabase();

4. Service layer

src/services/userService.ts

import { userDb } from "../database/users";
import { CreateUserDto, UpdateUserDto, User } from "../types/user";
export class UserService {
    async createUser(data: CreateUserDto): Promise<User> {
        this.validateEmail(data.email);
        this.validateAge(data.age);
        
        return userDb.create(data);
    }
    
    async getUsers(): Promise<User[]> {
        return userDb.findAll();
    }
    
    async getUserById(id: string): Promise<User | undefined> {
        return userDb.findById(id);
    }
    
    async updateUser(id: string, data: UpdateUserDto): Promise<User | undefined> {
        if (data.email) {
            this.validateEmail(data.email);
        }
        if (data.age !== undefined) {
            this.validateAge(data.age);
        }
        
        return userDb.update(id, data);
    }
    
    async deleteUser(id: string): Promise<boolean> {
        return userDb.delete(id);
    }
    
    private validateEmail(email: string): void {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(email)) {
            throw new Error("올바른 이메일 형식이 아닙니다");
        }
    }
    
    private validateAge(age: number): void {
        if (age < 0 || age > 150) {
            throw new Error("나이는 0-150 사이여야 합니다");
        }
    }
}
export const userService = new UserService();

5. Controllers

src/controllers/userController.ts

import { Request, Response } from "express";
import { userService } from "../services/userService";
import { ApiResponse } from "../types/api";
import { UserResponse } from "../types/user";
export class UserController {
    async createUser(req: Request, res: Response): Promise<void> {
        try {
            const user = await userService.createUser(req.body);
            
            const response: ApiResponse<UserResponse> = {
                success: true,
                data: {
                    ...user,
                    createdAt: user.createdAt.toISOString()
                }
            };
            
            res.status(201).json(response);
        } catch (error) {
            const response: ApiResponse<never> = {
                success: false,
                error: error instanceof Error ? error.message : "알 수 없는 에러"
            };
            res.status(400).json(response);
        }
    }
    
    async getUsers(req: Request, res: Response): Promise<void> {
        try {
            const users = await userService.getUsers();
            
            const response: ApiResponse<UserResponse[]> = {
                success: true,
                data: users.map(user => ({
                    ...user,
                    createdAt: user.createdAt.toISOString()
                }))
            };
            
            res.json(response);
        } catch (error) {
            const response: ApiResponse<never> = {
                success: false,
                error: error instanceof Error ? error.message : "알 수 없는 에러"
            };
            res.status(500).json(response);
        }
    }
    
    async getUserById(req: Request, res: Response): Promise<void> {
        try {
            const user = await userService.getUserById(req.params.id);
            
            if (!user) {
                const response: ApiResponse<never> = {
                    success: false,
                    error: "사용자를 찾을 수 없습니다"
                };
                res.status(404).json(response);
                return;
            }
            
            const response: ApiResponse<UserResponse> = {
                success: true,
                data: {
                    ...user,
                    createdAt: user.createdAt.toISOString()
                }
            };
            
            res.json(response);
        } catch (error) {
            const response: ApiResponse<never> = {
                success: false,
                error: error instanceof Error ? error.message : "알 수 없는 에러"
            };
            res.status(500).json(response);
        }
    }
    
    async updateUser(req: Request, res: Response): Promise<void> {
        try {
            const user = await userService.updateUser(req.params.id, req.body);
            
            if (!user) {
                const response: ApiResponse<never> = {
                    success: false,
                    error: "사용자를 찾을 수 없습니다"
                };
                res.status(404).json(response);
                return;
            }
            
            const response: ApiResponse<UserResponse> = {
                success: true,
                data: {
                    ...user,
                    createdAt: user.createdAt.toISOString()
                }
            };
            
            res.json(response);
        } catch (error) {
            const response: ApiResponse<never> = {
                success: false,
                error: error instanceof Error ? error.message : "알 수 없는 에러"
            };
            res.status(400).json(response);
        }
    }
    
    async deleteUser(req: Request, res: Response): Promise<void> {
        try {
            const deleted = await userService.deleteUser(req.params.id);
            
            if (!deleted) {
                const response: ApiResponse<never> = {
                    success: false,
                    error: "사용자를 찾을 수 없습니다"
                };
                res.status(404).json(response);
                return;
            }
            
            const response: ApiResponse<{ message: string }> = {
                success: true,
                data: { message: "사용자가 삭제되었습니다" }
            };
            
            res.json(response);
        } catch (error) {
            const response: ApiResponse<never> = {
                success: false,
                error: error instanceof Error ? error.message : "알 수 없는 에러"
            };
            res.status(500).json(response);
        }
    }
}
export const userController = new UserController();

6. Router

src/routes/userRoutes.ts

import { Router } from "express";
import { userController } from "../controllers/userController";
const router = Router();
router.post("/", (req, res) => userController.createUser(req, res));
router.get("/", (req, res) => userController.getUsers(req, res));
router.get("/:id", (req, res) => userController.getUserById(req, res));
router.put("/:id", (req, res) => userController.updateUser(req, res));
router.delete("/:id", (req, res) => userController.deleteUser(req, res));
export default router;

7. Entry point

src/index.ts

import express from "express";
import userRoutes from "./routes/userRoutes";
const app = express();
const PORT = 3000;
app.use(express.json());
app.use("/api/users", userRoutes);
app.get("/", (req, res) => {
    res.json({ message: "TypeScript API 서버" });
});
app.listen(PORT, () => {
    console.log(`서버 실행 중: http://localhost:${PORT}`);
});

8. Testing

Run the server

npm run dev

Try the API

# Create user
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"홍길동","email":"hong@test.com","age":25}'
# List users
curl http://localhost:3000/api/users
# Get one user
curl http://localhost:3000/api/users/U001
# Update user
curl -X PUT http://localhost:3000/api/users/U001 \
  -H "Content-Type: application/json" \
  -d '{"name":"김철수"}'
# Delete user
curl -X DELETE http://localhost:3000/api/users/U001

Summary

Takeaways

  1. Types: interface, type aliases, DTOs with Omit / Partial
  2. Layers: router → controller → service → data store
  3. Safety: invalid shapes fail at compile time when you type handlers
  4. Errors: try / catch with a consistent ApiResponse shape
  5. Production: compile with tsc, run emitted JS

Next steps

  • TypeScript + MongoDB
  • TypeScript + Prisma
  • Automated tests (Jest, Vitest)


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. TypeScript REST API tutorial: Express, tsconfig, DTOs (Omit/Partial), controller/service/database layers, ApiResponse JS…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers TypeScript, REST API, Express, Node.js, Backend, DTO.