스마트 Node.js 양식 유효성 검사

게시 됨: 2022-03-11

API에서 수행해야 하는 기본 작업 중 하나는 데이터 유효성 검사입니다. 이 기사에서는 멋지게 형식화된 데이터를 반환하는 방식으로 데이터에 대한 방탄 유효성 검사를 추가하는 방법을 보여 드리고자 합니다.

Node.js에서 사용자 지정 데이터 유효성 검사를 수행하는 것은 쉽지도 빠르지도 않습니다. 모든 종류의 데이터를 다루기 위해 작성해야 하는 기능이 많이 있습니다. Express 및 Koa 모두에 대해 몇 가지 Node.js 양식 데이터 라이브러리를 시도했지만 내 프로젝트의 요구 사항을 충족한 적이 없습니다. 복잡한 데이터 구조 또는 비동기 유효성 검사에서 작동하지 않는 라이브러리 및 확장 라이브러리에 문제가 있었습니다.

Datalize를 사용한 Node.js의 양식 유효성 검사

이것이 내가 결국 datalize라는 작지만 강력한 양식 유효성 검사 라이브러리를 작성하기로 결정한 이유입니다. 확장 가능하므로 모든 프로젝트에서 사용하고 요구 사항에 맞게 사용자 지정할 수 있습니다. 요청의 본문, 쿼리 또는 매개변수의 유효성을 검사합니다. 또한 async 필터 및 배열 또는 중첩 객체 와 같은 복잡한 JSON 구조를 지원합니다.

설정

Datalize는 npm을 통해 설치할 수 있습니다.

 npm install --save datalize

요청 본문을 구문 분석하려면 별도의 라이브러리를 사용해야 합니다. 아직 사용하지 않는다면 Koa의 경우 koa-body를, Express의 경우 body-parser를 권장합니다.

이 튜토리얼을 이미 설정된 HTTP API 서버에 적용하거나 다음과 같은 간단한 Koa HTTP 서버 코드를 사용할 수 있습니다.

 const Koa = require('koa'); const bodyParser = require('koa-body'); const app = new Koa(); const router = new (require('koa-router'))(); // helper for returning errors in routes app.context.error = function(code, obj) { this.status = code; this.body = obj; }; // add koa-body middleware to parse JSON and form-data body app.use(bodyParser({ enableTypes: ['json', 'form'], multipart: true, formidable: { maxFileSize: 32 * 1024 * 1024, } })); // Routes... // connect defined routes as middleware to Koa app.use(router.routes()); // our app will listen on port 3000 app.listen(3000); console.log(' API listening on 3000');

그러나 이것은 프로덕션 설정이 아니지만(로깅을 사용하고, 권한을 부여하고, 오류를 처리해야 합니다.) 이 몇 줄의 코드는 제가 보여드릴 예제에서 잘 작동합니다.

참고: 모든 코드 예제는 Koa를 사용하지만 데이터 유효성 검사 코드는 Express에서도 작동합니다. datalize 라이브러리에는 Express 양식 유효성 검사를 구현하는 예제도 있습니다.

기본 Node.js 양식 유효성 검사 예

Koa 또는 Express 웹 서버와 데이터베이스에 여러 필드가 있는 사용자를 생성하는 API의 끝점이 있다고 가정해 보겠습니다. 일부 필드는 필수이며 일부는 특정 값만 가질 수 있거나 올바른 유형으로 형식을 지정해야 합니다.

다음과 같이 간단한 논리를 작성할 수 있습니다.

 /** * @api {post} / Create a user * ... */ router.post('/', (ctx) => { const data = ctx.request.body; const errors = {}; if (!String(data.name).trim()) { errors.name = ['Name is required']; } if (!(/^[\-0-9a-zA-Z\.\+_]+@[\-0-9a-zA-Z\.\+_]+\.[a-zA-Z]{2,}$/).test(String(data.email))) { errors.email = ['Email is not valid.']; } if (Object.keys(errors).length) { return ctx.error(400, {errors}); } const user = await User.create({ name: data.name, email: data.email, }); ctx.body = user.toJSON(); });

이제 이 코드를 다시 작성하고 datalize를 사용하여 이 요청을 검증해 보겠습니다.

 const datalize = require('datalize'); const field = datalize.field; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email(), ]), (ctx) => { if (!ctx.form.isValid) { return ctx.error(400, {errors: ctx.form.errors}); } const user = await User.create(ctx.form); ctx.body = user.toJSON(); });

더 짧고 깔끔하며 읽기 쉽습니다. datalize를 사용하면 필드 목록을 지정하고 원하는 만큼 많은 규칙 (입력이 유효하지 않은 경우 오류를 발생시키는 함수) 또는 필터 (입력을 형식화하는 함수)에 연결할 수 있습니다.

규칙과 필터는 정의된 것과 동일한 순서로 실행되므로 먼저 공백에 대한 문자열을 트리밍한 다음 값이 있는지 확인하려면 .trim() 전에 .required() ) 을 정의해야 합니다.

그런 다음 Datalize는 지정한 필드만으로 개체(더 넓은 컨텍스트 개체에서 .form 으로 사용 가능)를 생성하므로 다시 나열할 필요가 없습니다. .form.isValid 속성은 유효성 검사가 성공했는지 여부를 알려줍니다.

자동 오류 처리

모든 요청에 ​​대해 양식이 유효한지 확인하고 싶지 않다면 데이터가 유효성 검사를 통과하지 못한 경우 요청을 취소하는 전역 미들웨어를 추가할 수 있습니다.

이를 위해 Koa/Express 앱 인스턴스를 생성하는 부트스트랩 파일 에 이 코드를 추가하면 됩니다.

 const datalize = require('datalize'); // set datalize to throw an error if validation fails datalize.set('autoValidate', true); // only Koa // add to very beginning of Koa middleware chain app.use(async (ctx, next) => { try { await next(); } catch (err) { if (err instanceof datalize.Error) { ctx.status = 400; ctx.body = err.toJSON(); } else { ctx.status = 500; ctx.body = 'Internal server error'; } } }); // only Express // add to very end of Express middleware chain app.use(function(err, req, res, next) { if (err instanceof datalize.Error) { res.status(400).send(err.toJSON()); } else { res.send(500).send('Internal server error'); } });

그리고 datalize가 우리를 대신해 데이터가 유효한지 더 이상 확인할 필요가 없습니다. 데이터가 유효하지 않으면 유효하지 않은 필드 목록과 함께 형식이 지정된 오류 메시지를 반환합니다.

쿼리 유효성 검사

예, 쿼리 매개변수를 매우 쉽게 검증할 수도 있습니다. POST 요청에만 사용할 필요는 없습니다. .query() 도우미 메서드를 사용하고 유일한 차이점은 데이터가 .form 대신 .data 객체에 저장된다는 것입니다.

 const datalize = require('datalize'); const field = datalize.field; /** * @api {get} / List users * ... */ router.post('/', datalize.query([ field('keywords').trim(), field('page').default(1).number(), field('perPage').required().select([10, 30, 50]), ]), (ctx) => { const limit = ctx.data.perPage; const where = { }; if (ctx.data.keywords) { where.name = {[Op.like]: ctx.data.keywords + '%'}; } const users = await User.findAll({ where, limit, offset: (ctx.data.page - 1) * limit, }); ctx.body = users; });

매개변수 유효성 검사를 위한 도우미 메서드인 .params() 도 있습니다. 쿼리 및 양식 데이터는 라우터의 .post() 메서드에서 두 개의 datalize 미들웨어를 전달하여 함께 검증할 수 있습니다.

더 많은 필터, 배열 및 중첩 개체

지금까지 Node.js 양식 유효성 검사에서 정말 간단한 데이터를 사용했습니다. 이제 배열, 중첩 객체 등과 같은 좀 더 복잡한 필드를 사용해 보겠습니다.

 const datalize = require('datalize'); const field = datalize.field; const DOMAIN_ERROR = "Email's domain does not have a valid MX (mail) entry in its DNS record"; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email().custom((value) => { return new Promise((resolve, reject) => { dns.resolve(value.split('@')[1], 'MX', function(err, addresses) { if (err || !addresses || !addresses.length) { return reject(new Error(DOMAIN_ERROR)); } resolve(); }); }); }), field('type').required().select(['admin', 'user']), field('languages').array().container([ field('id').required().id(), field('level').required().select(['beginner', 'intermediate', 'advanced']) ]), field('groups').array().id(), ]), async (ctx) => { const {languages, groups} = ctx.form; delete ctx.form.languages; delete ctx.form.groups; const user = await User.create(ctx.form); await UserGroup.bulkCreate(groups.map(groupId => ({ groupId, userId: user.id, }))); await UserLanguage.bulkCreate(languages.map(item => ({ languageId: item.id, userId: user.id, level: item.level, )); });

유효성을 검사해야 하는 데이터에 대한 기본 제공 규칙이 없는 경우 .custom() 메서드(훌륭한 이름, 맞죠?)를 사용하여 사용자 지정 데이터 유효성 검사 규칙을 만들고 거기에 필요한 로직을 작성할 수 있습니다. 중첩된 객체의 경우 datalize() 함수에서와 같은 방식으로 필드 목록을 지정할 수 있는 .container() 메서드가 있습니다. 컨테이너 내에 컨테이너를 중첩하거나 값을 배열로 변환하는 .array() 필터로 컨테이너를 보완할 수 있습니다. .array() 필터가 컨테이너 없이 사용되면 지정된 규칙이나 필터가 배열의 모든 값에 적용됩니다.

따라서 .array().select(['read', 'write']) 는 배열의 모든 값이 'read' 또는 'write' 인지 확인하고 그렇지 않은 경우 모든 인덱스 목록을 반환합니다. 오류. 꽤 멋지죠?

PUT / PATCH

PUT / PATCH (또는 POST )를 사용하여 데이터를 업데이트할 때 모든 논리, 규칙 및 필터를 다시 작성할 필요가 없습니다. .optional() 또는 .patch() 와 같은 추가 필터를 추가하기만 하면 요청에 정의되지 않은 경우 컨텍스트 개체에서 필드를 제거합니다. ( .optional() 은 항상 선택 사항으로 만드는 반면 .patch() 는 HTTP 요청의 메서드가 PATCH 인 경우에만 선택 사항으로 만듭니다.) 이 추가 필터를 추가하여 데이터베이스에서 데이터 생성 및 업데이트 모두에 작동하도록 할 수 있습니다.

 const datalize = require('datalize'); const field = datalize.field; const userValidator = datalize([ field('name').patch().trim().required(), field('email').patch().required().email(), field('type').patch().required().select(['admin', 'user']), ]); const userEditMiddleware = async (ctx, next) => { const user = await User.findByPk(ctx.params.id); // cancel request here if user was not found if (!user) { throw new Error('User was not found.'); } // store user instance in the request so we can use it later ctx.user = user; return next(); }; /** * @api {post} / Create a user * ... */ router.post('/', userValidator, async (ctx) => { const user = await User.create(ctx.form); ctx.body = user.toJSON(); }); /** * @api {put} / Update a user * ... */ router.put('/:id', userEditMiddleware, userValidator, async (ctx) => { await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); }); /** * @api {patch} / Patch a user * ... */ router.patch('/:id', userEditMiddleware, userValidator, async (ctx) => { if (!Object.keys(ctx.form).length) { return ctx.error(400, {message: 'Nothing to update.'}); } await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); });

두 개의 간단한 미들웨어로 모든 POST / PUT / PATCH 메소드에 대한 대부분의 로직을 작성할 수 있습니다. userEditMiddleware() 함수는 편집하려는 레코드가 있는지 확인하고 그렇지 않으면 오류를 발생시킵니다. 그런 다음 userValidator() 는 모든 끝점에 대한 유효성 검사를 수행합니다. 마지막으로 .patch() 필터는 정의되지 않고 요청의 메서드가 PATCH 인 경우 .form 객체에서 모든 필드를 제거합니다.

Node.js 양식 유효성 검사 추가 기능

사용자 정의 필터에서 다른 필드의 값을 가져오고 이를 기반으로 유효성 검사를 수행할 수 있습니다. 사용자 정의 함수 콜백 매개변수에서 모두 제공되므로 요청 또는 사용자 정보와 같은 컨텍스트 개체에서 모든 데이터를 가져올 수도 있습니다.

라이브러리는 기본 규칙 및 필터 세트를 다루지만 모든 필드에 사용할 수 있는 사용자 정의 전역 필터를 등록할 수 있으므로 동일한 코드를 반복해서 작성할 필요가 없습니다.

 const datalize = require('datalize'); const Field = datalize.Field; Field.prototype.date = function(format = 'YYYY-MM-DD') { return this.add(function(value) { const date = value ? moment(value, format) : null; if (!date || !date.isValid()) { throw new Error('%s is not a valid date.'); } return date.format(format); }); }; Field.prototype.dateTime = function(format = 'YYYY-MM-DD HH:mm') { return this.date(format); };

이 두 가지 사용자 정의 필터를 사용하면 필드를 .date() 또는 .dateTime() 필터와 연결하여 날짜 입력의 유효성을 검사할 수 있습니다.

datalize를 사용하여 파일의 유효성을 검사할 수도 있습니다. .file() , .mime().size() 와 같은 파일에만 특수 필터가 있으므로 파일을 별도로 처리할 필요가 없습니다.

지금 더 나은 API 작성 시작

소규모 및 대규모 API 모두에 대해 이미 여러 프로덕션 프로젝트에서 Node.js 양식 유효성 검사에 datalize를 사용하고 있습니다. 그것은 내가 더 읽기 쉽고 유지 관리하기 쉽게 만드는 동시에 스트레스를 덜 받으면서 훌륭한 프로젝트를 제시간에 제공하는 데 도움이 되었습니다. 한 프로젝트에서 Socket.IO 주위에 간단한 래퍼를 작성하여 WebSocket 메시지에 대한 데이터의 유효성을 검사하는 데 사용하기도 했습니다. 사용법은 Koa에서 경로를 정의하는 것과 거의 동일하므로 좋았습니다. 관심이 충분하다면 그에 대한 튜토리얼도 작성할 수 있습니다.

이 튜토리얼이 보안 문제나 내부 서버 오류 없이 완벽하게 검증된 데이터로 Node.js에서 더 나은 API를 빌드하는 데 이 튜토리얼이 도움이 되기를 바랍니다. 그리고 가장 중요한 것은 JavaScript를 사용하여 양식 유효성 검사를 위한 추가 기능을 작성하는 데 투자해야 하는 많은 시간을 절약할 수 있다는 것입니다.