Node.js - Express 복습

오랫만에 Express를 사용할 일이 생겼다.

기억이 가물가물해서 다시 한번 복습.

폴더 생성

mkdir my-api-node
cd my-api-node
yarn init -y
mkdir src

package install

컴파일 결과를 삭제하기 위해 rimraf를 global로 설치한다.

npm install rimraf -g

필수 package를 설치한다.

yarn add express body-parser cors morgan

yarn add -D eslint prettier nodemon rimraf
yarn add -D babel-core babel-eslint babel-jest @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread @babel/preset-env
yarn add -D eslint-config-prettier eslint-config-standard eslint-friendly-formatter eslint-loader eslint-plugin-import eslint-plugin-jest eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise eslint-plugin-standard

설정 파일 추가

.babelrc

{
  "presets": [
    ["@babel/env", {
      "targets": {
        "node": "current"
      }
    }]
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-proposal-object-rest-spread"
  ]
}

.eslintrc.js

module.exports = {
  parserOptions: {
    sourceType: 'module',
  },
  parser: 'babel-eslint',
  plugins: ['prettier'],
  extends: ['eslint:recommended', 'plugin:prettier/recommended', 'plugin:jest/recommended'],
  env: {
    node: true,
  },
  ignorePatterns: ['node_modules/'],
  rules: {
    'prettier/prettier': 'error',
    'newline-per-chained-call': 'error',
  },
};

.prettierrc.js

module.exports = {
  arrowParens: 'always',
  orderedImports: true,
  printWidth: 120,
  singleQuote: true,
  semi: true,
  tabWidth: 2,
  trailingComma: 'all',
  useTabs: false,
};

nodemon.json

{
  "restartable": "rs",
  "ignore": [
    ".git",
    "node_modules/**/node_modules",
    "dist"
  ],
  "verbose": true,
  "execMap": {
    "js": "node"
  },
  
  "runOnChangeOnly": false,
  "watch": [
    "src/**/*.js",
    "src/**/*.graphql",
    "src/**/*.gql"
  ],
  "env": {
    "NODE_ENV": "development"
  },
  "ext": "js,json,graphql"
}

package.json 파일에 scripts 추가

"scripts": {
  "build": "babel src --out-dir dist",
  "start": "node dist/index.js",
  "restart": "rimraf dist && yarn build && yarn start",
  "dev": "nodemon --exec yarn restart"
},

Express 기본 설정

src/server.js

import express from 'express';
import cors from 'cors';
import { json, urlencoded } from 'body-parser';
import morgan from 'morgan';

const app = express();

app.disable('x-powered-by');

app.use(cors());
app.use(json());
app.use(urlencoded({ extended: false }));
app.use(morgan('dev'));

export const start = () => {
  app.listen(5000, () => {
    console.log('server is on 5000');
  });
};

src/index.js

import { start } from './server';

start();

api 생성

간단한 get, post api를 만들어보자.

import express from 'express';
import cors from 'cors';
import { json, urlencoded } from 'body-parser';
import morgan from 'morgan';

const app = express();

app.disable('x-powered-by');

app.use(cors());
app.use(json());
app.use(urlencoded({ extended: false }));
app.use(morgan('dev'));

app.get('/', (req, res) => {
  res.send({ message: 'hello' });
});

app.post('/', (req, res) => {
  console.log(req.body);
  res.send(req.body);
});

export const start = () => {
  app.listen(5000, () => {
    console.log('server is on 5000');
  });
};

Custom middleware

import express from 'express';
import cors from 'cors';
import { json, urlencoded } from 'body-parser';
import morgan from 'morgan';

const app = express();

app.disable('x-powered-by');

app.use(cors());
app.use(json());
app.use(urlencoded({ extended: false }));
app.use(morgan('dev'));

const globalLog = (req, res, next) => {
  console.log('global logging');
  next();
};

app.use(globalLog);

const log = (req, res, next) => {
  console.log('logging');
  next();
};

app.get('/', log, (req, res) => {
  res.send({ message: 'hello' });
});

app.get('/data', [log, log, log], (req, res) => {
  res.send({ message: 'hello' });
});

app.post('/', (req, res) => {
  console.log(req.body);
  res.send(req.body);
});

export const start = () => {
  app.listen(5000, () => {
    console.log('server is on 5000');
  });
};

middleware를 위와 같은 방식으로 사용할 수 있다.

Sub router

src/resources/item 폴더를 생성하고 item.router.js 파일을 생성한다.

import { Router } from 'express';

const controller = (req, res) => {
  res.send({ message: 'hello' });
};
const router = Router();

// prettier-ignore
// /api/item
router.route("/")
  .get(controller)
  .post(controller);

// prettier-ignore
// /api/item/:id
router.route("/:id")
  .get(controller)
  .put(controller)
  .delete(controller);

export default router;

임시로 controller를 생성하고, 모든 router가 controller를 호출하도록 설정했다.

생성한 router를 server.js에서 사용한다.

import express from 'express';
import cors from 'cors';
import { json, urlencoded } from 'body-parser';
import morgan from 'morgan';
import itemRouter from './resources/item/item.router';

const app = express();

app.disable('x-powered-by');

app.use(cors());
app.use(json());
app.use(urlencoded({ extended: false }));
app.use(morgan('dev'));

app.use('/api/item', itemRouter);

export const start = () => {
  app.listen(5000, () => {
    console.log('server is on 5000');
  });
};

이렇게 설정하면 /api/item로 호출되는 end point는 itemRouter를 이용하게 된다.

route test 추가

테스트를 추가해보자.

jest를 설치한다.

yarn add -D jest

package.json 파일에 scripts를 추가한다.

"scripts": {
  "build": "babel src --out-dir dist",
  "start": "node dist/index.js",
  "restart": "rimraf dist && yarn build && yarn start",
  "dev": "nodemon --exec yarn restart",
  "test": "NODE_ENV=testing jest --forceExit --detectOpenHandles  --silent",
  "test-routes": "yarn test -t router"
},

테스트를 추가한다.

/src/resources/item/__tests__ 경로에 item.router.spec.js 파일을 생성한다.

import router from '../item.router';

describe('item router', () => {
  test('has crud routes', () => {
    const routes = [
      { path: '/', method: 'get' },
      { path: '/', method: 'post' },
      { path: '/:id', method: 'get' },
      { path: '/:id', method: 'put' },
      { path: '/:id', method: 'delete' },
    ];

    routes.forEach((route) => {
      const match = router.stack.find((s) => s.route.path === route.path && s.route.methods[route.method]);
      expect(match).toBeTruthy();
    });
  });
});

router.stack은 다음과 같이 생겼다.

[
  Layer {
    handle: [Function: bound dispatch],
    name: 'bound dispatch',
    params: undefined,
    path: undefined,
    keys: [],
    regexp: /^\/?$/i { fast_star: false, fast_slash: false },
    route: Route {
      path: '/',
      stack: [ [Layer], [Layer] ],
      methods: { get: true, post: true }
    }
  },
  Layer {
    handle: [Function: bound dispatch],
    name: 'bound dispatch',
    params: undefined,
    path: undefined,
    keys: [ { name: 'id', optional: false, offset: 1 } ],
    regexp: /^\/(?:([^\/]+?))\/?$/i { fast_star: false, fast_slash: false },
    route: Route {
      path: '/:id',
      stack: [ [Layer], [Layer], [Layer] ],
      methods: { get: true, put: true, delete: true }
    }
  }
]

router.stack의 정보를 이용하여 router test를 작성할 수 있다.


오랫만에 Node 코드를 보니 새롭네.

예전에 너무 설렁설렁했었나봐. 좀 더 자세히 보자.

꾸준히! 열심히!


Written by@[Suho]
뭐든지 만들어보자.