Deno 의 framework 를 사용하여 API 서버를 구축해보려한다. 현재 기준(2020/07/05) 수 많은 framework 가 있지만 oak 가 제일 인기 많고 잘 만들어져 있다고 한다. 사용법이 express 와 비슷해서 인기가 많아진 것 같지만 oak 에서 제공해주는 example 을 직접 구현해 보려 한다.
server 1 2 3 4 5 6 7 8 import { Application, } from "https://deno.land/x/oak/mod.ts" ; const app = new Application();console .log(`Server is listening on port 8000` );await app.listen({ port: 8000 });
oak 에서 제공해주는 Application 을 사용하여 서버를 실행한다. 이때 listen 함수는 promise 기 때문에 await 을 붙여줘서 실행하면 된다.
script 1 deno run --allow-net server.ts
이렇게 서버를 실행해보면 에러가 발생한다. 이 에러는 현재 request 에 대한 처리나 route 가 없어서 발생하는 에러이므로 일단 다음 단계로 넘어가서 추가해주면 된다.
routes 1 2 3 4 5 6 7 8 9 10 11 12 import { Router, } from "https://deno.land/x/oak/mod.ts" ; const router = new Router();app.use(router.routes()); app.use(router.allowedMethods()); router.get("/" , (context ) => { context.response.body = "Hello deno, oak" ; });
oak 에서 Router 를 가져와 router 를 생성해준다. 이후 app middleware 에 등록해 주는것이다. 이때 app.use 를 사용해서 등록하는데 express 와 동일하다.
router.routes() 는 router 의 path 를 등록해주는 것이고, allowedMethods() 는 해당 path 의 모든 http method 를 허용해 주는 것이다.
이후 express 의 router 와 동일하게 router.get 함수를 이용하여 router 를 등록해 줄 수 있다. 차이점은 두번째 parameter 함수다. 기존에는 (req, res, next) 형식의 parameter 를 사용했었는데, oak 는 이것을 context 하나의 객체로 사용한다. destructuring 을 사용하여 쓸 수도 있어 더 편해진것 같다.
이제 다시 서버를 시작해보면 에러가 사라지고 8000번에 접속해보면 정상 작동하는것을 확인할 수 있다.
route 와 server 분리를 위해 routes.ts 파일을 생성해준다.
1 2 3 4 5 6 7 8 9 10 11 import { Router, } from "https://deno.land/x/oak/mod.ts" ; const router = new Router();router.get("/" , (context ) => { context.response.body = "Hello deno, oak" ; }); export default router;
이후 server.ts 에서 해당 router 를 import 해준다.
1 2 3 4 5 6 7 8 9 10 11 12 13 import { Application, } from "https://deno.land/x/oak/mod.ts" ; import router from "./routes.ts" ;const app = new Application();app.use(router.routes()); app.use(router.allowedMethods()); console .log(`Server is listening on port 8000` );await app.listen({ port: 8000 });
route method 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 interface Book { id: string ; title: string ; author: string ; } let books: Book[] = [ { id: "1" , title: "Book 1" , author: "one" , }, { id: "2" , title: "Book 2" , author: "two" , }, { id: "3" , title: "Book 3" , author: "three" , }, ];
위 interface 구조를 가진 책 데이터를 사용하여 진행하려고 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 router .get("/" , ({ response } ) => { response.body = "Hello deno, oak" ; }) .get("/books" , ({ response } ) => { response.body = books; }) .get("/books/:id" , ({ params, response } ) => { const book: Book | undefined = books.find((book ) => book.id === params.id); if (book) { response.body = book; } else { response.body = "존재하지 않는 책" ; response.status = 404 ; } });
/books
로 get 요청이 들어오면 전체 책을 return 해준다. body 에 담아주면 된다.
/books/:id
로 get 요청이 들어오면 params 의 id 에 맞는 책을 찾아 return 해준다. 이때 express 와 다른점은 request 에 params 정보가 들어있는 것이 아니라 context 에서 따로 제공해주기 때문에 request 를 사용할 필요가 없다.
id 에 맞는 책을 찾아 리턴해주는데 존재하지 않은 책이면 status 를 404 로 리턴해준다.
post 1 2 3 4 5 6 7 8 9 10 11 12 13 14 .post("/books" , async ({ request, response }) => { const body = await request.body(); if (!request.hasBody) { response.status = 400 ; response.body = "데이터 없음" ; } else { const book: Book = body.value; book.id = v4.generate(); books.push(book); response.status = 201 ; response.body = book; } })
express 에서는 body-parser 를 사용하여 request.body 로 body 데이터를 받아오지만, oak 는 request.body promise 로 제공해준다. 또한 request.hasBody 를 사용하여 body 데이터가 있는지 확인할 수 있다.
DB 를 사용하는 경우 id 생성을 자동으로 해줄 수 있지만 지금은 std 의 uuid 를 사용하여 id 를 생성해줬다.
postman 으로 테스트해보면 정상 동작 하는것을 확인할 수 있다.
put 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .put("/books/:id" , async ({ params, request, response }) => { const bookIndex: number = books.findIndex((book ) => book.id === params.id); if (bookIndex < 0 ) { response.body = "존재하지 않는 책" ; response.status = 404 ; } else { if (!request.hasBody) { response.status = 400 ; response.body = "데이터 없음" ; } else { const body = await request.body(); const book: Book = body.value; const preBook: Book = books[bookIndex]; books.splice(bookIndex, 1 , { ...preBook, ...book }); response.status = 201 ; response.body = books[bookIndex]; } } })
Array.findIndex 를 사용하여 책을 찾아주고 수정해 주었다.
delete 1 2 3 4 5 6 7 8 9 10 .delete("/books/:id" , async ({ params, response }) => { const bookIndex: number = books.findIndex((book ) => book.id === params.id); if (bookIndex < 0 ) { response.body = "존재하지 않는 책" ; response.status = 404 ; } else { books.splice(bookIndex, 1 ); response.status = 200 ; } })
delete method 도 express 와 동일한 방식으로 사용가능 하다.
middleware logger 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { green, cyan, bold, } from "https://deno.land/std@0.60.0/fmt/colors.ts" ; import { Context, } from "https://deno.land/x/oak/mod.ts" ; const logger = async (context: Context, next: () => Promise <void >) => { await next(); const rt = context.response.headers.get("X-Response-Time" ); console .log( `${green(context.request.method)} ${ cyan(decodeURIComponent (context.request.url.pathname)) } - ${ bold( String (rt), ) } ` , ); }; export default logger;
1 2 3 4 5 6 7 8 9 10 11 12 import { Context, } from "https://deno.land/x/oak/mod.ts" ; const responseTime = async (context: Context, next: () => Promise <void >) => { const start = Date .now(); await next(); const ms = Date .now() - start; context.response.headers.set("X-Response-Time" , `${ms} ms` ); }; export default responseTime;
console 에 기록을 남기는 Logger 함수와 응답시간을 추적하는 ResponseTime 함수를 생성해 준 후 express middleware 등록과 같이 app.use 를 사용해서 등록해준다.
1 2 app.use(logger); app.use(responseTime);
404 (not found) 1 2 3 4 5 6 7 8 9 10 11 12 import { Status, Context, } from "https://deno.land/x/oak/mod.ts" ; const notFound = ({ request, response }: Context ) => { response.status = Status.NotFound; response.body = `<html><body><h1>404 - Not Found</h1><p>Path <code>${request.url} </code> not found.` ; }; export default notFound;
존재하지 않는 페이지에 접근했을때 실행되는 함수이다. route middleware 밑에 등록해주면 된다.
1 2 3 4 app.use(router.routes()); app.use(router.allowedMethods()); app.use(notFound);
error handler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { isHttpError, Context, } from "https://deno.land/x/oak/mod.ts" ; const errorHandler = async ( { request, response }: Context, next: () => Promise <void >, ) => { try { await next(); } catch (err) { if (isHttpError(err)) { response.status = err.status; const { message, status, stack } = err; if (request.accepts("json" )) { response.body = { message, status, stack }; response.type = "json" ; } else { response.body = `${status} ${message} \n\n${stack ?? "" } ` ; response.type = "text/plain" ; } } else { console .log(err); throw err; } } }; export default errorHandler;
errorHandler 함수를 만들어서 middleware 에 등록시켜준다. 추가로 router context 에 throw 라는 함수를 제공해준다. 이 함수를 실행하여 에러를 발생 시킬 수 있다.
1 context.throw(Status.NotFound, "존재하지 않는 책" );
Github code 보기 oak routing server