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
- Types:
interface, type aliases, DTOs withOmit/Partial - Layers: router → controller → service → data store
- Safety: invalid shapes fail at compile time when you type handlers
- Errors:
try/catchwith a consistentApiResponseshape - Production: compile with
tsc, run emitted JS
Next steps
- TypeScript + MongoDB
- TypeScript + Prisma
- Automated tests (Jest, Vitest)
Related posts
- TypeScript getting started | install, config, syntax
- Advanced TypeScript types | unions, intersections, literals
- TypeScript interfaces | complete guide
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.
Related Articles (Internal Links)
Other articles related to this topic.
- Advanced TypeScript | Conditional Types· Template Literals
- JavaScript Modules | ES Modules and CommonJS Explained
- TypeScript 실전 프로젝트 | REST API 서버 만들기
Keywords Covered in This Article (Related Search Terms)
This article covers TypeScript, REST API, Express, Node.js, Backend, DTO.