JUINTINATION
Express.js와 Prisma ORM을 사용한 CRUD API 본문
API란?
API(Application Programming Interface)란 정의 및 프로토콜 집합을 사용하여 두 소프트웨어 구성 요소가 서로 통신할 수 있게 하는 메커니즘이다.
API라는 용어를 처음 들었을 때 가장 많이 보게되는 예시 중 하나는 기상청일 것 같다. 여러 API와 데이터를 제공하는 공공데이터포털의 인기검색어만 살펴봐도 기상청이 가장 위에 뜨는 것을 확인할 수 있다.
API는 일반적으로 클라이언트와 서버 측면에서 설명되는데 요청을 보내는 애플리케이션을 클라이언트라고 하고 응답을 보내는 애플리케이션을 서버라고 할 때 기상청의 예에서 기상청의 날씨 데이터베이스는 서버이고 모바일 앱은 클라이언트입니다.
기상청의 API를 받아서 현재 날씨 정보 데이터를 받아 보여주는 서비스를 구현할 수도 있고 지난 2022년에 과제로 제출한 프로젝트인 채팅 프로그램 HalkTalk에서 추가 기능으로 현재 날씨를 띄워주는 것과 같이 사용할 수 있다.
REST API란?
REST(Representational State Transfer)는 클라이언트가 서버 데이터에 액세스하는 데 사용할 수 있는 GET, PUT, DELETE 등의 함수 집합이다. REST API의 주된 특징은 서버가 요청 간에 클라이언트 데이터를 저장하지 않음을 의미하는 무상태이다. 클라이언트와 서버는 HTTP를 사용하여 데이터를 교환하는데 서버에 대한 클라이언트 요청은 웹 사이트를 방문하기 위해 브라우저에 입력하는 URL과 유사하며 서버의 응답은 웹 페이지의 일반적인 그래픽 렌더링이 없는 일반 데이터이다.
웹 API란?
웹 API, 또는 웹 서비스 API는 웹 서버와 웹 브라우저 간의 애플리케이션 처리 인터페이스로 모든 웹 서비스는 API이지만 모든 API가 웹 서비스는 아니다. REST API는 위에서 설명한 표준 아키텍처 스타일을 사용하는 특수한 유형의 웹 API인 것이다.
REST API를 사용했을 때의 이점
REST API는 다음과 같은 네 가지 주요 이점을 제공한다.
- 통합
- API는 새로운 애플리케이션을 기존 소프트웨어 시스템과 통합하는 데 사용된다.
- API를 사용하여 기존 코드를 활용할 수 있다.
- 각 기능을 처음부터 작성할 필요가 없기 때문에 개발 속도가 빨라진다.
- 혁신
- 새로운 앱의 등장으로 전체 산업이 바뀔 수 있는데 기업은 이에 신속하게 대응하고 혁신적인 서비스의 신속한 배포를 지원해야 한다.
- 전체 코드를 다시 작성할 필요 없이 API 수준에서 변경하여 이를 수행할 수 있다.
- 확장
- API는 기업이 다양한 플랫폼에서 고객의 요구 사항을 충족할 수 있는 고유한 기회를 제공한다.
- 예를 들어 지도 API를 사용하면 웹 사이트, Android, iOS 등을 통해 지도 정보를 통합할 수 있으며 어느 기업이나 무료 또는 유료 API를 사용하여 내부 데이터베이스에 유사한 액세스 권한을 부여할 수 있다.
- API는 기업이 다양한 플랫폼에서 고객의 요구 사항을 충족할 수 있는 고유한 기회를 제공한다.
- 유지 관리의 용이성
- API는 두 시스템 간의 게이트웨이 역할을 하며 API가 영향을 받지 않도록 각 시스템은 내부적으로 변경해야 한다.
- 이렇게 하면 한 시스템의 향후 코드 변경이 다른 시스템에 영향을 미치지 않는다.
CRUD란?
CRUD는 대부분의 소프트웨어가 가지는 기본적인 데이터 처리 기능인 Create(생성), Read(읽기), Update(갱신), Delete(삭제)를 묶어서 일컫는 말이다. 사용자 인터페이스가 갖추어야 할 기능(정보의 참조/검색/갱신)을 가리키는 용어로서도 사용된다.
각 문자는 다음과 같이 표준 SQL문으로 대응 가능하다.
이름 | 조작 | SQL |
Create | 생성 | INSERT |
Read(또는 Retrieve) | 읽기(또는 인출) | SELECT |
Update | 갱싱 | UPDATE |
Delete(또는 Destroy) | 삭제(또는 파괴) | DELETE |
Express.js로 CRUD API 구현하기
이제 드디어 Express로 CRUD API를 구현할 것이다. 지난 Express.js와 Prisma ORM + MySQL 글에서 정리 및 사용했던 것들을 그대로 사용할 것이다. 먼저 내가 어제 구현했던 app.js의 전체 코드는 다음과 같다. Github에도 crud-test로 올려놨으니 만약 코드가 수정되거나 추가될 수 있으니 한 번씩 확인해 보면 좋을 것 같다. 참고로 일부러 MVC 패턴은 적용하지 않았으며 나보다 먼저 스터디를 시작한 친구의 코드를 참고했다.
const express = require('express');
const http = require('http');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const app = express();
app.set('port', process.env.PORT || 3000);
app.use(express.json());
const router = express.Router();
app.use(router);
app.use(express.static(__dirname + '/public'));
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
// 전체 유저 조회(READ)
const getAllUsers = async () => {
try {
return await prisma.user.findMany();
} catch (err) {
console.error('Error in getAllUsers: ', err.stack);
throw new Error('Failed to get all users');
}
};
const getAllUserInfo = async () => {
try {
return await getAllUsers();
} catch (err) {
console.error('Error in getAllUsersInfo: ', err.stack);
throw new Error('Failed to get all users');
}
};
router.get('/getAllUsers', async (req, res) => {
try {
const users = await getAllUserInfo();
res.status(200).json(users);
} catch (err) {
console.error('Error is getAllUsers route: ', err.stack);
res.status(500).json({ error: 'Internal Server Error' });
}
});
// 특정 유저 조회(READ)
const getUserById = async (userId) => {
try {
return await prisma.user.findUnique({ where: { id: userId }});
} catch (err) {
console.error('Error in getUserById: ', err.stack);
throw new Error('Failed to get user by ID');
}
};
const getUserInfoById = async (userId) => {
try {
return await getUserById(userId);
} catch (err) {
console.error('Error in getUserById: ', err.stack);
throw new Error('Failed to get user by ID');
}
};
router.get('/getUserById/:userId', async (req, res) => {
try {
const userId = parseInt(req.params.userId);
const user = await getUserInfoById(userId);
res.status(200).json(user);
} catch (err) {
console.error('Error is getUserById route: ', err.stack);
res.status(500).json({ error: 'Internal Server Error' });
}
});
// 새로운 유저 추가(CREATE)
const createUser = async (userData) => {
try {
return await prisma.user.create({
data: userData,
})
} catch (err) {
console.error('Error in createUser: ', err.stack);
throw new Error('Failed to create user');
}
};
const createUserByData = async (userData) => {
try {
return await createUser(userData);
} catch (err) {
console.error('Error in createUserByData: ', err.stack);
throw new Error('Failed to create user');
}
};
router.post('/createUser', async (req, res) => {
try {
const user = await createUserByData(req.body);
res.status(200).json(user);
} catch (err) {
console.error('Error in createUserByData route: ', err.stack);
res.status(500).json({ error: 'Failed to create user' });
}
});
// 유저 정보 수정(UPDATE)
const updateUser = async (userId, userData) => {
try {
return await prisma.user.update({
where: { id: userId },
data: userData,
})
} catch (err) {
console.error('Error in createUser: ', err.stack);
throw new Error('Failed to update user');
}
};
const updateUserById = async (userId, userData) => {
try {
return await updateUser(userId, userData);
} catch (err) {
console.error('Error in updateUserById: ', err.stack);
throw new Error('Failed to update user');
}
};
router.put('/updateUser/:userId', async (req, res) => {
try {
const userId = parseInt(req.params.userId);
const updatedUser = await updateUserById(userId, req.body);
res.status(200).json(updatedUser);
} catch (err) {
console.error('Error is updateUserById route: ', err.stack);
res.status(500).json({ error: 'Internal Server Error' });
}
});
// 유저 삭제(DELETE)
const deleteUser = async (userId) => {
try {
return await prisma.user.delete({
where: { id: userId },
})
} catch (err) {
console.error('Error in createUser: ', err.stack);
throw new Error('Failed to delete user');
}
};
const deleteUserById = async (userId) => {
try {
return await deleteUser(userId);
} catch (err) {
console.error('Error in deleteUserById: ', err.stack);
throw new Error('Failed to update user');
}
};
router.delete('/deleteUser/:userId', async (req, res) => {
try {
const userId = parseInt(req.params.userId);
await deleteUserById(userId, req.body);
res.status(204).end();
} catch (err) {
console.error('Error is deleteUserById route: ', err.stack);
res.status(500).json({ error: 'Internal Server Error' });
}
});
http.createServer(app).listen(app.get('port'), () => {
console.log('Express server listening on port ' + app.get('port'));
});
참고로 index.html은 다음과 같이 아주 간단하게 작성했다.
<h1>Welcome page</h1>
<p>This is index.html</p>
먼저 테스트를 위해 사용중인 db인 Prisma.io 홈페이지에서 사용한 예시 테이블의 관계를 나타낸 ERD는 아래와 같다.
API 테스트를 할 때 위의 테이블 구조를 만족하도록 해야하므로 보고 가는 것이 좋다. USER 테이블로 테스트할 때 role 속성이 "USER", "ADMIN" 2개만 가능하다는 사실은 명시되어있지 않지만 해당 홈페이지에 있는 코드를 읽어보면 알 수 있다.
나는 제대로 읽지 않아서 조금 헤맸다.
작성한 코드 해석
- Express 설정:
const express = require('express');
: Express.js 프레임워크를 가져온다.const http = require('http');
: 서버를 생성하기 위한 HTTP 모듈을 가져온다.const { PrismaClient } = require('@prisma/client');
: Prisma 클라이언트 모듈을 가져온다.const prisma = new PrismaClient();
: Prisma 클라이언트의 인스턴스를 생성한다.const app = express();
: Express 애플리케이션을 생성한다.app.set('port', process.env.PORT || 3000);
: 애플리케이션의 포트를 process.env.PORT로, 없다면 3000으로 설정한다.
- 미들웨어 및 정적 파일:
app.use(express.json());
: 수신된 JSON 요청을 파싱하기 위한 미들웨어를 사용한다.app.use(express.static(\_\_dirname + '/public'));
: /public 디렉터리의 정적 파일을 제공하도록 설정한다.app.get('/', (req, res) => {
: 서버를 실행한 후에 localhost:{app.get(’port’)에 접속하게 되면 ./public 디렉터리에 있는 index.html을 제공한다.
res.sendFile(\_\_dirname + '/index.html');
});
- 라우터:
const router = express.Router();
: Express 라우터를 생성한다.app.use(router);
: app에서 라우터를 사용한다.router.get('/getAllUsers', ...)
: localhost:{app.get('port')}/getAllUsers/에서 모든 유저를 가져오는 라우트를 처리한다.router.get('/getUserById/:userId', ...)
: localhost:{app.get('port')}/getUserById/:userId/에서 특정 id를 가지는 유저를 가져오는 라우트를 처리한다.router.post('/createUser', ...)
: localhost:{app.get('port')}/createUser/에서 새로운 유저를 생성하는 라우트를 처리한다.router.put('/updateUser/:userId', ...)
: localhost:{app.get('port')}/updateUser/:userId에서 특정 id를 가지는 유저의 정보를 업데이트하는 라우트를 처리한다.router.delete('/deleteUser/:userId', ...)
: localhost:{app.get('port')}/deleteUser/:userId/에서 특정 id를 가지는 유저를 삭제하는 라우트를 처리한다.
- 데이터베이스 작업:
getAllUsers
,getUserById
,createUser
,updateUser
,deleteUser
와 같은 함수는 Prisma 클라이언트를 사용하여 데이터베이스 작업을 수행한다.
- 서버 생성:
http.createServer(app).listen(app.get('port'), ...)
: HTTP 서버를 생성한다.
- 오류 처리:
- try-catch 블록을 사용하여 오류 처리가 구현되어 있으며 모든 http 상태코드에 대한 오류 응답이 아닌 서버가 예상하지 못한 상황에 놓였다는 것을 나타내는 500의 오류 응답만 반환되며 추후에 추가할 예정이다.
API 테스트
나는 CRUD API 테스트를 위해 Postman을 사용했다.
위와 같이 CRUD-TEST라는 Collection을 만들고 POST, GET, DELETE, PUT 각 http 메서드에 대한 Request를 생성해준 뒤에 테스트를 진행했다. 관련 내용은 나중에 추가하도록 하겠다.
먼저 GET 메서드를 테스트해보자. 현재 실행중인 서버의 포트번호는 3000이므로 localhost:3000/getAllUsers/ 를 입력하고 우측 상단의 Send 버튼을 누르면 다음과 같이 정상적으로 현재 DB에 저장되어있는 test1에 대한 정보가 뜨는 것을 확인할 수 있다.
다음으로 POST 메서드를 테스트해보자. localhost:3000/createUser/ 를 입력하고 아래와 같이 해당 추가할 내용을 테이블에 맞게 json 파일을 작성해준 뒤에 우측 상단의 Send 버튼을 누르면 다음과 같이 정상적으로 User 테이블에 새로운 유저가 추가된 것을 확인할 수 있다.
이번엔 DELETE 메서드를 테스트해보자. localhost:3000/deleteUser/2/ 를 입력하고 우측 상단의 Send 버튼을 누르면 다음과 같이 정상적으로 User 테이블에서 id가 2인 유저가 삭제된 것을 확인할 수 있다.
마지막으로 PUT 메서드를 테스트해보자. localhost:3000/updateUser/1/ 를 입력하고 아래와 같이 "name"을 "test1"에서 "test_put"으로 바꾸도록 json 파일을 작성한 뒤에 우측 상단의 Send 버튼을 누르면 다음과 같이 정상적으로 User 테이블에서 id가 1인 유저의 "name"이 "test1"에서 "test_put"으로 바뀐 것을 확인할 수 있다.
결론
이번에 스터디를 하면서 Tableplus도 그렇고 Postman도, 게다가 Express.js까지 처음 써보는 것들 투성이인데 벌써 간단하게나마 CRUD API까지 구현에 성공했다. 오로지 공부를 위해 MVC 패턴을 적용하지 않고 하나의 app.js 파일에 모든 코드를 작성하는 과정에서 오류도 많이 발생하고 Postman을 사용했을 때 json 파일이 제대로 파싱되지 않기도 하는 문제를 해결해가면서 힘들긴 했지만 개발에 더 흥미가 생긴 것 같아 좋은 시간이었던 것 같다. 최대한 오늘 안으로 MVC 패턴까지 정리하고 기본적인 스퍼트 프로젝트의 틀을 잡아보도록 하겠다. 그래야 일요일은 조금이나마 쉴 수 있을 것 같다.
'StudyNote' 카테고리의 다른 글
Express.js의 morgan과 cookie-parser, express-session (2) | 2024.01.21 |
---|---|
Express.js과 MVC 패턴, Controller & Service & Repository (3) | 2024.01.20 |
Node.js와 Express.js 가볍게 입문해보기 - 4 (0) | 2024.01.19 |
Express.js와 Prisma ORM + MySQL (0) | 2024.01.15 |
Node.js와 Express.js 가볍게 입문해보기 - 3 (1) | 2024.01.15 |