Node.js/TypeScript REST API 빌드, 2부: 모델, 미들웨어 및 서비스
게시 됨: 2022-03-11REST API 시리즈의 첫 번째 기사에서 npm을 사용하여 백엔드를 처음부터 생성하고, TypeScript와 같은 종속성을 추가하고, Node.js에 내장된 debug 모듈을 사용하고, Express.js 프로젝트 구조를 빌드하고, 런타임을 기록하는 방법을 다루었습니다. 이벤트는 Winston과 함께 유연하게 진행됩니다. 이러한 개념에 이미 익숙하다면 이것을 복제하고 git checkout 을 사용하여 toptal-article-01 분기로 전환한 다음 계속 읽으십시오.
REST API 서비스, 미들웨어, 컨트롤러 및 모델
약속한 대로 이제 다음 모듈에 대해 자세히 알아보겠습니다.
- 비즈니스 로직 작업을 미들웨어와 컨트롤러가 호출할 수 있는 기능으로 캡슐화하여 코드를 더 깔끔하게 만드는 서비스 입니다.
- Express.js가 적절한 컨트롤러 기능을 호출하기 전에 전제 조건을 검증하는 미들웨어 .
- 요청자에게 최종적으로 응답을 보내기 전에 서비스를 사용하여 요청을 처리하는 컨트롤러 .
- 데이터를 설명하고 컴파일 시간 검사를 지원하는 모델 입니다.
또한 프로덕션에 적합하지 않은 매우 기초적인 데이터베이스도 추가할 것입니다. (그 유일한 목적은 이 튜토리얼을 더 쉽게 따라할 수 있도록 하여 다음 기사에서 데이터베이스 연결 및 MongoDB 및 Mongoose와의 통합에 대해 자세히 알아볼 수 있도록 하는 것입니다.)
실습: DAO, DTO 및 임시 데이터베이스의 첫 단계
튜토리얼의 이 부분에서 데이터베이스는 파일도 사용하지 않습니다. 단순히 사용자 데이터를 배열에 보관할 것입니다. 즉, Node.js를 종료할 때마다 데이터가 증발합니다. 가장 기본적인 CRUD( 만들기, 읽기, 업데이트 및 삭제 ) 작업만 지원합니다.
여기서는 두 가지 개념을 사용할 것입니다.
- 데이터 액세스 개체(DAO)
- 데이터 전송 개체(DTO)
두문자어 간의 한 글자 차이는 필수적입니다. DAO는 정의된 데이터베이스에 연결하고 CRUD 작업을 수행하는 역할을 합니다. DTO는 DAO가 데이터베이스로 보내고 받을 원시 데이터를 보유하는 개체입니다.
즉, DTO는 데이터 모델 유형을 준수하는 개체이고 DAO는 이를 사용하는 서비스입니다.
예를 들어 중첩된 데이터베이스 엔터티를 나타내는 DTO가 더 복잡해질 수 있지만 이 기사에서 단일 DTO 인스턴스는 단일 데이터베이스 행에 대한 특정 작업에 해당합니다.
왜 DTO인가?
DTO를 사용하여 TypeScript 개체가 데이터 모델을 따르도록 하면 아래 서비스 섹션에서 볼 수 있듯이 아키텍처 일관성을 유지하는 데 도움이 됩니다. 그러나 중요한 경고가 있습니다. DTO나 TypeScript 자체는 런타임에 발생해야 하기 때문에 자동 사용자 입력 유효성 검사를 약속하지 않습니다. 코드가 API의 끝점에서 사용자 입력을 수신하면 해당 입력은 다음을 수행할 수 있습니다.
- 추가 필드가 있습니다.
- 필수 필드가 누락되었습니다(즉,
?접미사가 없는 필드). - TypeScript를 사용하여 모델에서 지정한 유형이 아닌 데이터가 있는 필드가 있습니다.
TypeScript(및 변환되는 JavaScript)는 우리를 위해 이것을 마술처럼 확인하지 않으므로 특히 API를 공개적으로 열 때 이러한 유효성 검사를 잊지 않는 것이 중요합니다. ajv와 같은 패키지는 이를 도와줄 수 있지만 일반적으로 기본 TypeScript가 아닌 라이브러리별 스키마 개체에서 모델을 정의하여 작동합니다. (다음 글에서 다룰 몽구스는 이번 프로젝트에서도 비슷한 역할을 할 것이다.)
"더 간단한 것보다 DAO와 DTO를 모두 사용하는 것이 정말 최선입니까?"라고 생각할 수 있습니다. 엔터프라이즈 개발자 Gunther Popp이 답을 제공합니다. 중기적으로 확장할 것으로 합리적으로 기대할 수 없다면 대부분의 소규모 실제 Express.js/TypeScript 프로젝트에서 DTO를 피하고 싶을 것입니다.
그러나 프로덕션에서 사용하지 않을 경우에도 이 예제 프로젝트는 TypeScript API 아키텍처를 마스터하는 과정에서 가치 있는 기회입니다. TypeScript 유형을 추가 방식으로 활용하고 DTO와 작업하여 구성 요소 및 모델을 추가할 때 더 기본적인 접근 방식과 비교하는 방법을 확인하는 좋은 방법입니다.
TypeScript 수준의 사용자 REST API 모델
먼저 사용자에 대해 세 가지 DTO를 정의합니다. users 폴더 안에 dto 라는 폴더를 만들고 다음을 포함하는 create.user.dto.ts 라는 파일을 만듭니다.
export interface CreateUserDto { id: string; email: string; password: string; firstName?: string; lastName?: string; permissionLevel?: number; }데이터베이스에 관계없이 사용자를 생성할 때마다 ID, 비밀번호, 이메일이 있어야 하며 선택적으로 이름과 성이 있어야 합니다. 이러한 요구 사항은 주어진 프로젝트의 비즈니스 요구 사항에 따라 변경될 수 있습니다.
PUT 요청의 경우 전체 개체를 업데이트하려고 하므로 이제 선택적 필드가 필요합니다. 같은 폴더에서 다음 코드를 사용하여 put.user.dto.ts 라는 파일을 만듭니다.
export interface PutUserDto { id: string; email: string; password: string; firstName: string; lastName: string; permissionLevel: number; } PATCH 요청의 경우 TypeScript의 Partial 기능을 사용할 수 있습니다. 이 기능은 다른 유형을 복사하고 모든 필드를 선택 사항으로 만들어 새 유형을 생성합니다. 그렇게 하면 patch.user.dto.ts 파일에 다음 코드만 포함하면 됩니다.
import { PutUserDto } from './put.user.dto'; export interface PatchUserDto extends Partial<PutUserDto> {} 이제 메모리 내 임시 데이터베이스를 생성해 보겠습니다. users 폴더 안에 daos 라는 폴더를 만들고 users.dao.ts 라는 파일을 추가해 봅시다.
먼저 생성한 DTO를 가져오려고 합니다.
import { CreateUserDto } from '../dto/create.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; import { PutUserDto } from '../dto/put.user.dto';이제 사용자 ID를 처리하기 위해 (터미널을 사용하여) shortid 라이브러리를 추가해 보겠습니다.
npm i shortid npm i --save-dev @types/shortid users.dao.ts로 users.dao.ts 를 가져올 것입니다.
import shortid from 'shortid'; import debug from 'debug'; const log: debug.IDebugger = debug('app:in-memory-dao'); 이제 다음과 같은 UsersDao 라는 클래스를 만들 수 있습니다.
class UsersDao { users: Array<CreateUserDto> = []; constructor() { log('Created new instance of UsersDao'); } } export default new UsersDao(); 싱글톤 패턴을 사용하여 이 클래스는 다른 파일로 가져올 때 항상 동일한 인스턴스를 제공하고, 중요한 것은 동일한 users 배열을 제공합니다. Node.js가 이 파일을 가져올 때마다 캐시하고 모든 가져오기가 시작 시 발생하기 때문입니다. 즉, users.dao.ts 를 참조하는 모든 파일에는 Node.js가 이 파일을 처음 처리할 때 내보내지는 동일한 new UsersDao() 에 대한 참조가 전달됩니다.
이 기사에서 이 클래스를 더 사용하고 프로젝트 전체의 대부분의 클래스에 이 공통 TypeScript/Express.js 패턴을 사용할 때 이것이 작동하는 것을 볼 수 있습니다.
참고: 싱글톤의 자주 언급되는 단점은 단위 테스트를 작성하기 어렵다는 것입니다. 많은 클래스의 경우 재설정이 필요한 클래스 멤버 변수가 없기 때문에 이 단점이 적용되지 않습니다. 그러나 그것이 필요한 사람들을 위해 독자가 종속성 주입을 사용하여 이 문제에 접근하는 것을 고려할 수 있는 연습으로 남겨둡니다.
이제 기본 CRUD 작업을 클래스에 함수로 추가할 것입니다. 생성 기능은 다음과 같습니다.
async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; }읽기 는 "모든 리소스 읽기"와 "ID별로 읽기"의 두 가지 방식으로 제공됩니다. 다음과 같이 코딩됩니다.
async getUsers() { return this.users; } async getUserById(userId: string) { return this.users.find((user: { id: string }) => user.id === userId); } 마찬가지로 업데이트 는 전체 객체를 덮어쓰거나( PUT 으로) 객체의 일부만( PATCH 로) 덮어쓰는 것을 의미합니다.
async putUserById(userId: string, user: PutUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1, user); return `${user.id} updated via put`; } async patchUserById(userId: string, user: PatchUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); let currentUser = this.users[objIndex]; const allowedPatchFields = [ 'password', 'firstName', 'lastName', 'permissionLevel', ]; for (let field of allowedPatchFields) { if (field in user) { // @ts-ignore currentUser[field] = user[field]; } } this.users.splice(objIndex, 1, currentUser); return `${user.id} patched`; } 앞서 언급했듯이 이러한 함수 서명의 UserDto 선언에도 불구하고 TypeScript는 런타임 유형 검사를 제공하지 않습니다. 이는 다음을 의미합니다.
-
putUserById()에 버그가 있습니다. 이를 통해 API 소비자는 DTO에서 정의한 모델의 일부가 아닌 필드에 대한 값을 저장할 수 있습니다. -
patchUserById()는 모델과 동기화해야 하는 필드 이름의 중복 목록에 따라 다릅니다. 이것이 없으면 이 목록에 대해 업데이트되는 개체를 사용해야 합니다. 이는 DTO 정의 모델의 일부이지만 이 특정 개체 인스턴스에 대해 이전에 저장되지 않은 필드의 값을 자동으로 무시한다는 것을 의미합니다.
그러나 이 두 시나리오는 다음 기사에서 데이터베이스 수준에서 올바르게 처리됩니다.
리소스를 삭제 하는 마지막 작업은 다음과 같습니다.
async removeUserById(userId: string) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1); return `${userId} removed`; }보너스로 사용자 생성의 전제 조건은 사용자 이메일이 중복되지 않았는지 확인하는 것이므로 이제 "이메일로 사용자 가져오기" 기능을 추가해 보겠습니다.
async getUserByEmail(email: string) { const objIndex = this.users.findIndex( (obj: { email: string }) => obj.email === email ); let currentUser = this.users[objIndex]; if (currentUser) { return currentUser; } else { return null; } }참고: 실제 시나리오에서는 필요한 모든 기본 작업을 추상화하는 Mongoose 또는 Sequelize와 같은 기존 라이브러리를 사용하여 데이터베이스에 연결할 수 있습니다. 이 때문에 위에서 구현한 기능에 대한 세부 사항은 다루지 않습니다.
REST API 서비스 계층
이제 기본 메모리 내 DAO가 있으므로 CRUD 함수를 호출하는 서비스를 만들 수 있습니다. CRUD 함수는 데이터베이스에 연결하는 모든 서비스에 있어야 하는 것이므로 새 서비스를 구현하려고 할 때마다 구현하려는 메서드가 포함된 CRUD 인터페이스를 만들 것입니다.
오늘날 우리가 사용하는 IDE에는 우리가 구현하는 기능을 추가하는 코드 생성 기능이 있어 작성해야 하는 반복적인 코드의 양을 줄입니다.
WebStorm IDE를 사용하는 빠른 예:
IDE는 MyService 클래스 이름을 강조 표시하고 다음 옵션을 제안합니다.
"모든 구성원 구현" 옵션은 CRUD 인터페이스를 준수하는 데 필요한 기능을 즉시 스캐폴드합니다.
즉, 먼저 CRUD 라는 TypeScript 인터페이스를 생성해 보겠습니다. common 폴더에서 interfaces 라는 폴더를 만들고 다음과 함께 crud.interface.ts 를 추가해 보겠습니다.
export interface CRUD { list: (limit: number, page: number) => Promise<any>; create: (resource: any) => Promise<any>; putById: (id: string, resource: any) => Promise<string>; readById: (id: string) => Promise<any>; deleteById: (id: string) => Promise<string>; patchById: (id: string, resource: any) => Promise<string>; } 완료되면 users 폴더 내에 services 폴더를 만들고 여기에 다음을 포함하는 users.service.ts 파일을 추가할 수 있습니다.
import UsersDao from '../daos/users.dao'; import { CRUD } from '../../common/interfaces/crud.interface'; import { CreateUserDto } from '../dto/create.user.dto'; import { PutUserDto } from '../dto/put.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; class UsersService implements CRUD { async create(resource: CreateUserDto) { return UsersDao.addUser(resource); } async deleteById(id: string) { return UsersDao.removeUserById(id); } async list(limit: number, page: number) { return UsersDao.getUsers(); } async patchById(id: string, resource: PatchUserDto) { return UsersDao.patchUserById(id, resource); } async readById(id: string) { return UsersDao.getUserById(id); } async putById(id: string, resource: PutUserDto) { return UsersDao.putUserById(id, resource); } async getUserByEmail(email: string) { return UsersDao.getUserByEmail(email); } } export default new UsersService(); 여기서 우리의 첫 번째 단계는 메모리 내 DAO, 인터페이스 종속성 및 각 DTO의 TypeScript 유형을 가져오는 것이었습니다. 이제 DAO에서 사용한 것과 동일한 패턴인 UsersService 를 서비스 싱글톤으로 구현할 차례입니다.
모든 CRUD 함수는 이제 UsersDao 의 해당 함수를 호출합니다. DAO를 교체할 때가 되면 3부에서 볼 수 있듯이 DAO 함수가 호출되는 이 파일에 대한 약간의 조정을 제외하고 프로젝트의 다른 곳에서 변경할 필요가 없습니다.
예를 들어 list() 에 대한 모든 호출을 추적하고 교체하기 전에 컨텍스트를 확인할 필요가 없습니다. 이것이 바로 위에서 볼 수 있는 소량의 초기 상용구를 희생시키면서 이러한 분리 계층을 갖는 이점입니다.
비동기/대기 및 Node.js
서비스 기능에 대한 async 사용은 무의미해 보일 수 있습니다. 현재로서는 다음과 같습니다. 이러한 모든 함수는 내부적으로 Promise 또는 await 를 사용하지 않고 즉시 값을 반환합니다. 이것은 async 를 사용할 서비스에 대한 코드베이스를 준비하기 위한 것입니다. 마찬가지로 아래에서 이러한 함수에 대한 모든 호출이 await 를 사용하는 것을 볼 수 있습니다.
이 기사가 끝나면 실험할 실행 가능한 프로젝트를 다시 갖게 될 것입니다. 코드베이스의 여러 위치에 다양한 유형의 오류를 추가하고 컴파일 및 테스트 중에 어떤 일이 발생하는지 볼 수 있는 좋은 시간이 될 것입니다. 특히 async 컨텍스트의 오류는 예상대로 작동하지 않을 수 있습니다. 이 기사의 범위를 벗어나는 다양한 솔루션을 파고들고 탐색할 가치가 있습니다.
이제 DAO와 서비스가 준비되었으면 사용자 컨트롤러로 돌아가 보겠습니다.
REST API 컨트롤러 구축
위에서 말했듯이 컨트롤러 뒤에 있는 아이디어는 경로 요청을 최종적으로 처리하는 코드에서 경로 구성을 분리하는 것입니다. 즉, 요청이 컨트롤러에 도달하기 전에 모든 유효성 검사가 완료되어야 합니다. 컨트롤러는 실제 요청으로 무엇을 해야 하는지만 알면 됩니다. 요청이 그렇게까지 했다면 유효한 것으로 판명되었음을 알기 때문입니다. 그런 다음 컨트롤러는 처리할 각 요청의 해당 서비스를 호출합니다.
시작하기 전에 사용자 비밀번호를 안전하게 해싱하기 위한 라이브러리를 설치해야 합니다.
npm i argon2 users 컨트롤러 폴더 안에 controllers 라는 폴더를 만들고 그 안에 users.controller.ts 라는 파일을 만드는 것으로 시작해 보겠습니다.

// we import express to add types to the request/response objects from our controller functions import express from 'express'; // we import our newly created user services import usersService from '../services/users.service'; // we import the argon2 library for password hashing import argon2 from 'argon2'; // we use debug with a custom context as described in Part 1 import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersController { async listUsers(req: express.Request, res: express.Response) { const users = await usersService.list(100, 0); res.status(200).send(users); } async getUserById(req: express.Request, res: express.Response) { const user = await usersService.readById(req.body.id); res.status(200).send(user); } async createUser(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); const userId = await usersService.create(req.body); res.status(201).send({ id: userId }); } async patch(req: express.Request, res: express.Response) { if (req.body.password) { req.body.password = await argon2.hash(req.body.password); } log(await usersService.patchById(req.body.id, req.body)); res.status(204).send(); } async put(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); log(await usersService.putById(req.body.id, req.body)); res.status(204).send(); } async removeUser(req: express.Request, res: express.Response) { log(await usersService.deleteById(req.body.id)); res.status(204).send(); } } export default new UsersController(); 참고: HTTP 204 No Content 응답과 함께 아무 것도 반환하지 않는 위의 줄은 주제에 대한 RFC 7231과 일치합니다.
사용자 컨트롤러 싱글톤이 완료되면 예제 REST API 개체 모델 및 서비스에 의존하는 다른 모듈인 사용자 미들웨어를 코딩할 준비가 되었습니다.
Express.js가 포함된 Node.js REST 미들웨어
Express.js 미들웨어로 무엇을 할 수 있습니까? 유효성 검사는 매우 적합합니다. 요청이 사용자 컨트롤러에 전달되기 전에 요청에 대한 게이트키퍼 역할을 하는 몇 가지 기본 유효성 검사를 추가해 보겠습니다.
- 사용자를 생성하거나 업데이트하는 데 필요한
email및password와 같은 사용자 필드가 있는지 확인합니다. - 주어진 이메일이 이미 사용 중이 아닌지 확인
- 생성 후
email필드를 변경하지 않는지 확인합니다(단순성을 위해 이를 기본 사용자 대면 ID로 사용하기 때문에). - 주어진 사용자가 있는지 확인
이러한 유효성 검사가 Express.js에서 작동하도록 하려면 이전 기사에서 설명한 대로 next() 를 사용하여 흐름 제어의 Express.js 패턴을 따르는 함수로 변환해야 합니다. 새 파일 users/middleware/users.middleware.ts 가 필요합니다.
import express from 'express'; import userService from '../services/users.service'; import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersMiddleware { } export default new UsersMiddleware();익숙한 싱글톤 상용구를 제거하고 일부 미들웨어 기능을 클래스 본문에 추가해 보겠습니다.
async validateRequiredUserBodyFields( req: express.Request, res: express.Response, next: express.NextFunction ) { if (req.body && req.body.email && req.body.password) { next(); } else { res.status(400).send({ error: `Missing required fields email and password`, }); } } async validateSameEmailDoesntExist( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user) { res.status(400).send({ error: `User email already exists` }); } else { next(); } } async validateSameEmailBelongToSameUser( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user && user.id === req.params.userId) { next(); } else { res.status(400).send({ error: `Invalid email` }); } } // Here we need to use an arrow function to bind `this` correctly validatePatchEmail = async ( req: express.Request, res: express.Response, next: express.NextFunction ) => { if (req.body.email) { log('Validating email', req.body.email); this.validateSameEmailBelongToSameUser(req, res, next); } else { next(); } }; async validateUserExists( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.readById(req.params.userId); if (user) { next(); } else { res.status(404).send({ error: `User ${req.params.userId} not found`, }); } } API 소비자가 새로 추가된 사용자에 대한 추가 요청을 쉽게 할 수 있도록 요청 URL 자체에서 들어오는 요청 매개변수에서 userId 를 추출하는 도우미 함수를 추가하고 이를 나머지 사용자 데이터가 있는 요청 본문입니다.
여기서 아이디어는 매번 매개변수에서 ID를 가져오는 것에 대해 걱정할 필요 없이 사용자 정보를 업데이트하려는 경우 전체 본문 요청을 간단히 사용할 수 있다는 것입니다. 대신 미들웨어라는 한 곳에서 처리됩니다. 기능은 다음과 같습니다.
async extractUserId( req: express.Request, res: express.Response, next: express.NextFunction ) { req.body.id = req.params.userId; next(); } 논리 외에도 미들웨어와 컨트롤러의 주요 차이점은 이제 next() 함수를 사용하여 구성된 기능의 체인을 따라 제어를 전달하는 것이 최종 목적지(우리의 경우 컨트롤러)에 도달할 때까지라는 것입니다.
모든 것을 통합하기: 경로 리팩토링
이제 프로젝트 아키텍처의 모든 새로운 측면을 구현했으므로 이전 기사에서 정의한 users.routes.config.ts 파일로 돌아가 보겠습니다. 그것은 우리의 미들웨어와 컨트롤러를 호출할 것이고, 둘 다 우리의 사용자 서비스에 의존하고, 차례로 우리의 사용자 모델이 필요합니다.
최종 파일은 다음과 같이 간단합니다.
import { CommonRoutesConfig } from '../common/common.routes.config'; import UsersController from './controllers/users.controller'; import UsersMiddleware from './middleware/users.middleware'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes(): express.Application { this.app .route(`/users`) .get(UsersController.listUsers) .post( UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailDoesntExist, UsersController.createUser ); this.app.param(`userId`, UsersMiddleware.extractUserId); this.app .route(`/users/:userId`) .all(UsersMiddleware.validateUserExists) .get(UsersController.getUserById) .delete(UsersController.removeUser); this.app.put(`/users/:userId`, [ UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailBelongToSameUser, UsersController.put, ]); this.app.patch(`/users/:userId`, [ UsersMiddleware.validatePatchEmail, UsersController.patch, ]); return this.app; } } 여기에서 비즈니스 로직을 검증하는 미들웨어와 모든 것이 유효한 경우 요청을 처리하기 위한 적절한 컨트롤러 기능을 추가하여 경로를 재정의했습니다. 또한 Express.js의 .param() 함수를 사용하여 userId 를 추출했습니다.
.all() 함수에서 GET , PUT , PATCH 또는 DELETE 가 엔드포인트 /users/:userId 를 통과하기 전에 호출되도록 UsersMiddleware 의 validateUserExists 함수를 전달합니다. 이것은 validateUserExists 가 .put() 또는 .patch() 에 전달하는 추가 함수 배열에 있을 필요가 없다는 것을 의미합니다. 이는 거기에 지정된 함수보다 먼저 호출됩니다.
여기에서도 미들웨어의 고유한 재사용성을 다른 방식으로 활용했습니다. POST 및 PUT 컨텍스트 모두에서 사용할 UsersMiddleware.validateRequiredUserBodyFields 를 전달하여 다른 미들웨어 기능과 우아하게 재결합합니다.
면책 조항: 이 문서에서는 기본 유효성 검사만 다룹니다. 실제 프로젝트에서는 코딩에 필요한 모든 제한 사항을 생각하고 찾아야 합니다. 단순화를 위해 사용자가 이메일을 변경할 수 없다고 가정합니다.
Express/TypeScript REST API 테스트
이제 Node.js 앱을 컴파일하고 실행할 수 있습니다. 일단 실행되면 Postman 또는 cURL과 같은 REST 클라이언트를 사용하여 API 경로를 테스트할 준비가 된 것입니다.
먼저 사용자를 확보해 보겠습니다.
curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json'이 시점에서 응답으로 빈 배열을 갖게 되며 이는 정확합니다. 이제 다음을 사용하여 첫 번째 사용자 리소스를 만들 수 있습니다.
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json'이제 Node.js 앱이 미들웨어에서 오류를 다시 보냅니다.
{ "error": "Missing required fields email and password" } 이 문제를 해결하기 위해 /users 리소스에 유효한 게시 요청을 보내겠습니다.
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23" }'이번에는 다음과 같은 내용이 표시되어야 합니다.
{ "id": "ksVnfnPVW" } 이 id 는 새로 생성된 사용자의 식별자이며 컴퓨터에서 다릅니다. 나머지 테스트 문을 더 쉽게 만들기 위해 얻은 명령으로 이 명령을 실행할 수 있습니다(Linux와 유사한 환경을 사용한다고 가정).
REST_API_EXAMPLE_ 이제 위의 변수를 사용하여 GET 요청을 함으로써 얻은 응답을 볼 수 있습니다.
curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' 이제 다음 PUT 요청으로 전체 리소스를 업데이트할 수도 있습니다.
curl --request PUT "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23", "firstName": "Marcos", "lastName": "Silva", "permissionLevel": 8 }'이메일 주소를 변경하여 유효성 검사가 작동하는지 테스트할 수도 있습니다. 그러면 오류가 발생합니다.
리소스 ID에 PUT 을 사용할 때 표준 REST 패턴을 따르려면 API 소비자로서 전체 개체를 보내야 합니다. 즉, lastName 필드만 업데이트하지만 PUT 끝점을 사용하면 업데이트할 전체 개체를 보내야 합니다. lastName 필드만 보내는 표준 REST 제약 조건 내에 있기 때문에 PATCH 요청을 사용하는 것이 더 쉬울 것입니다.
curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }' 우리 자신의 코드베이스에서 우리가 이 기사에서 추가한 미들웨어 기능을 사용하여 PUT 과 PATCH 를 구분하는 것은 우리의 경로 구성이라는 것을 기억하십시오.
PUT , PATCH 또는 둘 다?
PATCH 의 유연성을 감안할 때 PUT 을 지원할 이유가 많지 않은 것처럼 들릴 수 있으며 일부 API는 이러한 접근 방식을 취할 것입니다. 다른 사람들은 API를 "완전히 REST 호환"으로 만들기 위해 PUT 지원을 주장할 수 있습니다. 이 경우 필드별 PUT 경로를 만드는 것이 일반적인 사용 사례에 적합한 전술이 될 수 있습니다.
실제로, 이러한 요점은 둘 사이의 실제 차이점에서 PATCH 단독에 대한 보다 유연한 의미에 이르기까지 훨씬 더 큰 논의의 일부입니다. 여기에서 PUT 지원과 널리 사용되는 PATCH 의미 체계를 간단하게 제시하지만 독자가 준비가 되었을 때 추가 연구를 탐구하도록 권장합니다.
위에서 했던 것처럼 사용자 목록을 다시 가져오면 필드가 업데이트된 생성된 사용자가 표시되어야 합니다.
[ { "id": "ksVnfnPVW", "email": "[email protected]", "password": "$argon2i$v=19$m=4096,t=3,p=1$ZWXdiTgb922OvkNAdh9acA$XUXsOHaRN4uVg5ltIwwO+SPLxvb9uhOKcxoLER1e/mM", "firstName": "Marcos", "lastName": "Faraco", "permissionLevel": 8 } ]마지막으로 다음과 같이 사용자 삭제를 테스트할 수 있습니다.
curl --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'사용자 목록을 다시 가져오면 삭제된 사용자가 더 이상 존재하지 않는 것을 볼 수 있습니다.
이를 통해 users 리소스에 대한 모든 CRUD 작업이 작동합니다.
Node.js/TypeScript REST API
시리즈의 이 부분에서는 Express.js를 사용하여 REST API를 빌드하는 주요 단계를 더 자세히 살펴보았습니다. 서비스, 미들웨어, 컨트롤러 및 모델을 지원하기 위해 코드를 분할합니다. 각 기능에는 유효성 검사, 논리적 작업 또는 유효한 요청 처리 및 응답과 같은 특정 역할이 있습니다.
우리는 또한 이 시점에서 몇 가지 테스트를 허용한 다음 시리즈의 다음 부분에서 더 실용적인 것으로 교체할 목적 으로 데이터를 저장하는 매우 간단한 방법을 만들었습니다.
예를 들어 싱글톤 클래스를 사용하여 단순성을 염두에 두고 API를 구축하는 것 외에도 유지 관리, 확장성 및 보안을 더 쉽게 만들기 위해 취해야 할 몇 가지 단계가 있습니다. 시리즈의 마지막 기사에서는 다음을 다룹니다.
- 메모리 내 데이터베이스를 MongoDB로 교체한 다음 Mongoose를 사용하여 코딩 프로세스 단순화
- JWT를 사용하여 상태 비저장 방식으로 보안 계층 추가 및 액세스 제어
- 애플리케이션을 확장할 수 있도록 자동화된 테스트 구성
여기에서 이 기사의 최종 코드를 찾아볼 수 있습니다.
