fastify를 사용하는 데모 API
소개
나는 배울 새로운 기술과 새로운 좋은 것들을 찾고 있었고 그것이 내가 fastify에 대해 알게 된 방법입니다.
언뜻 보기에는 Express와 상당히 유사한 프레임워크이지만, 더 나은benchmarks 기능도 있고 몇 가지 꽤 좋은 기능도 있으므로 사용해 보기로 하고 이 게시물을 작성하게 된 이유입니다.
이 게시물은 업데이트되었으며 코드는 이제 fastify 3.x에서 작동합니다.
이 게시물에 대한 모든 코드는 here 입니다.
시작하자
프로젝트 구조:
./migrations
./src
/plugins
/routes
/api
/persons
/index.js
/schemas.js
/products
/index.js
/schemas.js
/docs
/index.js
/services
/persons.js
/products.js
/utils
/functions.js
/app.js
/environment.js
/start.js
./knexfile.js
./이전:
이 경우에는 knex 쿼리 빌더를 사용하고 있는데, 저에게는 꽤 괜찮고 마이그레이션 및 시드 작업이 쉽습니다. 제품(20200124230315_create_Person_table.js) 테이블에 대한 마이그레이션 파일이 하나만 있고 다음과 같습니다.
const up = knex => {
return knex.schema.hasTable('Person').then(exists => {
if (!exists) {
return knex.schema.createTable('Person', table => {
table.increments('id');
table.string('name', 100).notNullable();
table.string('lastName', 100).defaultTo(null);
table.string('document', 15).notNullable();
table.string('genre', 1).notNullable();
table.integer('phone').unsigned().notNullable();
table.timestamps(true, true);
table.unique('document');
});
}
});
};
const down = knex => {
return knex.schema.dropTable('Person');
};
module.exports = {
up,
down
};
Note: For a quick start about the migrations or seeds, you can see this gist.
./src/플러그인:
fastify의 중요한 측면은 플러그인 개념입니다. 코드 기능을 캡슐화한 다음 fastify 앱 개체에서 사용하는 것이 좋습니다. knex 연결용 플러그인과 mongo 연결용 플러그인이 두 개 있습니다.
./src/plugins/knex-db-connector.js:
const fastifyPlugin = require('fastify-plugin');
const knex = require('knex');
const {
DB_SQL_CLIENT,
DB_SQL_HOST,
DB_SQL_USER,
DB_SQL_PASSWORD,
DB_SQL_NAME,
DB_SQL_PORT
} = require('../environment');
const knexConnector = async (server, options = {}) => {
const db = knex({
client: DB_SQL_CLIENT,
connection: {
host: DB_SQL_HOST,
user: DB_SQL_USER,
password: DB_SQL_PASSWORD,
database: DB_SQL_NAME,
port: DB_SQL_PORT,
...options.connection
},
...options
});
server.decorate('knex', db);
};
// Wrapping a plugin function with fastify-plugin exposes the decorators,
// hooks, and middlewares declared inside the plugin to the parent scope.
module.exports = fastifyPlugin(knexConnector);
./src/plugins/mongo-db-connector.js:
const {
DB_NOSQL_USER,
DB_NOSQL_PASSWORD,
DB_NOSQL_HOST,
DB_NOSQL_NAME
} = require('../environment');
const MONGO_URL = `mongodb+srv://${DB_NOSQL_USER}:${DB_NOSQL_PASSWORD}@${DB_NOSQL_HOST}/${DB_NOSQL_NAME}?retryWrites=true&w=majority`;
const mongoConnector = app => {
app.register(require('fastify-mongodb'), {
// force to close the mongodb connection when app stopped
// the default value is false
forceClose: true,
url: MONGO_URL
});
};
module.exports = mongoConnector;
Note: In this case I'm using fastify-mongodb to help me with the connection. At this moment it's throwing a warning about some deprecated methods, that's a problem that will have a solution on comming updates from fastify-mongodb.
./src/경로:
Express에서와 마찬가지로 fastify의 경로에는 요청 개체 및 응답 개체(fastify에 대한 응답)가 있는 경로 처리기가 있지만 fastify에서 경로는 JSON Schema을 사용하여 유효성 검사 및 직렬화와 같은 다른 기능을 가질 수 있습니다.
./src/routes/api/index.js:
const oas = require('fastify-swagger');
const apiRoutes = async (app, options) => {
app.register(oas, require('../docs'));
app.register(require('./persons'), { prefix: 'persons' });
app.register(require('./products'), { prefix: 'products' });
app.get('/', async (request, reply) => {
return { hello: 'world' };
});
};
module.exports = apiRoutes;
Note: As you can see I have a GET route ‘/’ and it has a route handler, also I’m using as plugins the other routes for persons and products endpoints, I describe them below, and finally I’m using fastify-swagger, don’t worry I’m going to explain that.
./src/routes/api/persons/index.js:
const { PersonService } = require('../../../services/persons');
const { createSchema, getAllSchema, getOneSchema, updateSchema, deleteSchema } = require('./schemas');
const personRoutes = async (app, options) => {
const personService = new PersonService(app);
// create
app.post('/', { schema: createSchema }, async (request, reply) => {
const { body } = request;
const created = await personService.create({ person: body });
return created;
});
// get all
app.get('/', { schema: getAllSchema }, async (request, reply) => {
app.log.info('request.query', request.query);
const persons = await personService.getAll({});
return persons;
});
// get one
app.get('/:personId', { schema: getOneSchema }, async (request, reply) => {
const { params: { personId } } = request;
app.log.info('personId', personId);
const person = await personService.getOne({ id: personId });
return person;
});
// update
app.patch('/:personId', { schema: updateSchema }, async (request, reply) => {
const { params: { personId } } = request;
const { body } = request;
app.log.info('personId', personId);
app.log.info('body', body);
const updated = await personService.update({ id: personId, person: body });
return updated;
});
// delete
app.delete('/:personId', { schema: deleteSchema }, async (request, reply) => {
const { params: { personId } } = request;
app.log.info('personId', personId);
const deleted = await personService.delete({ id: personId });
return deleted;
});
};
module.exports = personRoutes;
Note: Here I have all the endpoints related to persons, I’m using the schema for each endpoint and also I’m using the PersonService, I'll explain that.
./src/routes/api/persons/schemas.js:
const personProperties = {
id: { type: 'number' },
name: { type: 'string' },
lastName: { type: 'string', nullable: true },
document: { type: 'string' },
genre: {
type: 'string',
enum: ['M', 'F']
},
phone: { type: 'number', maximum: 9999999999 },
created_at: { type: 'string' },
updated_at: { type: 'string' }
};
const tags = ['person'];
const paramsJsonSchema = {
type: 'object',
properties: {
personId: { type: 'number' }
},
required: ['personId']
};
const queryStringJsonSchema = {
type: 'object',
properties: {
filter: { type: 'string' }
},
required: ['filter']
};
const bodyCreateJsonSchema = {
type: 'object',
properties: personProperties,
required: ['name', 'document', 'genre', 'phone']
};
const bodyUpdateJsonSchema = {
type: 'object',
properties: personProperties
};
const getAllSchema = {
tags,
querystring: queryStringJsonSchema,
response: {
200: {
type: 'array',
items: {
type: 'object',
properties: personProperties
}
}
}
};
const getOneSchema = {
tags,
params: paramsJsonSchema,
querystring: queryStringJsonSchema,
response: {
200: {
type: 'object',
properties: personProperties
}
}
};
const createSchema = {
tags,
body: bodyCreateJsonSchema,
response: {
201: {
type: 'object',
properties: personProperties
}
}
};
const updateSchema = {
tags,
params: paramsJsonSchema,
body: bodyUpdateJsonSchema,
response: {
200: {
type: 'object',
properties: personProperties
}
}
};
const deleteSchema = {
tags,
params: paramsJsonSchema,
response: {
200: {
type: 'object',
properties: personProperties
}
}
};
module.exports = {
getAllSchema,
getOneSchema,
createSchema,
updateSchema,
deleteSchema
};
Note: Here are all the schemas for each endpoint, using these schemas I can have some importans validations, for example, a body attribute can't be a string if was defined as number, the maximun length of a string attribute, etc.
./src/routes/api/products/index.js:
const { ProductService } = require('../../../services/products');
const {
createSchema,
getAllSchema,
getOneSchema,
updateSchema,
deleteSchema
} = require('./schemas');
const productRoutes = async (app, options) => {
const productService = new ProductService(app);
// create
app.post('/', { schema: createSchema }, async (request, reply) => {
const { body } = request;
const insertedId = await productService.create({ product: body });
app.log.info('insertedId', insertedId);
return { _id: insertedId };
});
// get all
app.get('/', { schema: getAllSchema }, async (request, reply) => {
app.log.info('request.query', request.query);
const products = await productService.getAll({ filter: {} });
return products;
});
// get one
app.get('/:productId', { schema: getOneSchema }, async (request, reply) => {
const { params: { productId } } = request;
app.log.info('productId', productId);
const product = await productService.getOne({ id: productId });
return product;
});
// update
app.patch('/:productId', { schema: updateSchema }, async (request, reply) => {
const { params: { productId } } = request;
const { body } = request;
app.log.info('productId', productId);
app.log.info('body', body);
const updated = await productService.update({ id: productId, product: body });
return updated;
});
// delete
app.delete('/:productId', { schema: deleteSchema }, async (request, reply) => {
const { params: { productId } } = request;
app.log.info('productId', productId);
const deleted = await productService.delete({ id: productId });
return deleted;
});
};
module.exports = productRoutes;
./src/routes/api/products/schemas.js:
const productProperties = {
_id: { type: 'string' },
name: { type: 'string' },
description: { type: 'string' },
image: { type: 'string', nullable: true },
price: { type: 'number', maximum: 9999999999 }
};
const tags = ['product'];
const paramsJsonSchema = {
type: 'object',
properties: {
productId: { type: 'string' }
},
required: ['productId']
};
const queryStringJsonSchema = {
type: 'object',
properties: {
filter: { type: 'string' }
},
required: ['filter']
};
const bodyCreateJsonSchema = {
type: 'object',
properties: productProperties,
required: ['name', 'description', 'price']
};
const bodyUpdateJsonSchema = {
type: 'object',
properties: productProperties
};
const getAllSchema = {
tags,
querystring: queryStringJsonSchema,
response: {
200: {
type: 'array',
items: {
type: 'object',
properties: productProperties
}
}
}
};
const getOneSchema = {
tags,
params: paramsJsonSchema,
querystring: queryStringJsonSchema,
response: {
200: {
type: 'object',
properties: productProperties
}
}
};
const createSchema = {
tags,
body: bodyCreateJsonSchema,
response: {
201: {
type: 'object',
properties: productProperties
}
}
};
const updateSchema = {
tags,
params: paramsJsonSchema,
body: bodyUpdateJsonSchema,
response: {
200: {
type: 'object',
properties: productProperties
}
}
};
const deleteSchema = {
tags,
params: paramsJsonSchema,
response: {
200: {
type: 'object',
properties: productProperties
}
}
};
module.exports = {
getAllSchema,
getOneSchema,
createSchema,
updateSchema,
deleteSchema
};
./src/routes/docs/index.js:
const { APP_PORT } = require('../../environment');
module.exports = {
routePrefix: '/documentation',
exposeRoute: true,
swagger: {
info: {
title: 'fastify demo api',
description: 'docs',
version: '0.1.0'
},
externalDocs: {
url: 'https://swagger.io',
description: 'Find more info here'
},
servers: [
{ url: `http://localhost:${APP_PORT}`, description: 'local development' },
{ url: 'https://dev.your-site.com', description: 'development' },
{ url: 'https://sta.your-site.com', description: 'staging' },
{ url: 'https://pro.your-site.com', description: 'production' }
],
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json'],
tags: [
{ name: 'person', description: 'Person related end-points' },
{ name: 'product', description: 'Product related end-points' }
]
}
};
Note: Is the turn to talk about fastify-swagger, It's a module that helps us creating the API documentation, using this “configuration file” and the schemas used before, for each endpoint.
./src/서비스:
이 폴더에는 끝점에서 필요한 모든 논리를 해결하기 위해 담당하는 "클래스"가 포함되어 있습니다.
./src/services/persons.js:
const { isEmptyObject } = require('../utils/functions');
class PersonService {
/**
* Creates an instance of PersonService.
* @param {object} app fastify app
* @memberof PersonService
*/
constructor (app) {
if (!app.ready) throw new Error(`can't get .ready from fastify app.`);
this.app = app;
const { knex } = this.app;
if (!knex) {
throw new Error('cant get .knex from fastify app.');
}
}
/**
* function to create one
*
* @param { {person: object} } { person }
* @returns {Promise<number>} created id
* @memberof PersonService
*/
async create ({ person }) {
const err = new Error();
if (!person) {
err.statusCode = 400;
err.message = 'person is needed.';
throw err;
}
const { knex } = this.app;
const id = (await knex('Person').insert(person))[0];
const createdPerson = await this.getOne({ id });
return createdPerson;
}
/**
* function to get all
*
* @param { filter: object } { filter = {} }
* @returns {Promise<{ id: number }>[]} array
* @memberof PersonService
*/
async getAll ({ filter = {} }) {
const { knex } = this.app;
const persons = await knex.select('*').from('Person').where(filter);
return persons;
}
/**
* function to get one
*
* @param { { id: number } } { id }
* @returns {Promise<{id: number}>} object
* @memberof PersonService
*/
async getOne ({ id }) {
const err = new Error();
if (!id) {
err.message = 'id is needed';
err.statusCode = 400;
throw err;
}
const { knex } = this.app;
const data = await knex.select('*').from('Person').where({ id });
if (!data.length) {
err.statusCode = 412;
err.message = `can't get the person ${id}.`;
throw err;
}
const [person] = data;
return person;
}
/**
* function to update one
*
* @param { { id: number, person: object } } { id, person = {} }
* @returns {Promise<{ id: number }>} updated
* @memberof PersonService
*/
async update ({ id, person = {} }) {
const personBefore = await this.getOne({ id });
if (isEmptyObject(person)) {
return personBefore;
}
const { knex } = this.app;
await knex('Person')
.update(person)
.where({ id: personBefore.id });
const personAfter = await this.getOne({ id });
return personAfter;
}
/**
* function to delete one
*
* @param { { id: number } } { id }
* @returns {Promise<object>} deleted
* @memberof PersonService
*/
async delete ({ id }) {
const personBefore = await this.getOne({ id });
const { knex } = this.app;
await knex('Person').where({ id }).delete();
delete personBefore.id;
return personBefore;
}
}
module.exports = {
PersonService
};
Note: As you noticed, this is the service that I’m using in the person routes, by constructor I'm passing the fastify app and then, from the fastify app I get the knex connector to keep in touch with the database.
./src/services/products.js:
const { ObjectId } = require('mongodb');
class ProductService {
/**
* Creates an instance of ProductService.
* @param {object} app fastify app
* @memberof ProductService
*/
constructor (app) {
if (!app.ready) throw new Error(`can't get .ready from fastify app.`);
this.app = app;
const { mongo } = this.app;
if (!mongo) {
throw new Error('cant get .mongo from fastify app.');
}
const db = mongo.db;
const collection = db.collection('Product');
this.collection = collection;
}
/**
* function to create one
*
* @param {{ product: object }} { product }
* @returns {Promise<{ id: number }>} created
* @memberof ProductService
*/
async create ({ product }) {
const { insertedId } = (await this.collection.insertOne(product));
const created = await this.getOne({ id: insertedId });
return created;
}
/**
* function to get all
*
* @param {{ filter: object }} { filter = {} }
* @returns {Promise<{ id: number }> []} array
* @memberof ProductService
*/
async getAll ({ filter = {} }) {
const products = await this.collection.find(filter).toArray();
return products;
}
/**
* function to get one
*
* @param {{ id: number }} { id }
* @returns {Promise<{ id: number }>}
* @memberof ProductService
*/
async getOne ({ id }) {
const err = new Error();
if (!id) {
err.statusCode = 400;
err.message = 'id is needed.';
throw err;
}
const product = await this.collection.findOne({ _id: ObjectId(id) });
if (!product) {
err.statusCode = 400;
err.message = `can't get the product ${id}.`;
throw err;
}
return product;
}
/**
* function to update one
*
* @param {{ id: number, product: object }} { id, product }
* @returns {Promise<{ id: number }>} updated
* @memberof ProductService
*/
async update ({ id, product }) {
await this.getOne({ id });
const { upsertedId } = (await this.collection.updateOne(
{
_id: ObjectId(id)
},
{
$set: product
},
{
upsert: true
}
));
const after = await this.getOne({ upsertedId });
return after;
}
/**
* function to delete one
*
* @param {{ id: number }} { id }
* @returns {Promise<object>} deleted
* @memberof ProductService
*/
async delete ({ id }) {
const before = await this.getOne({ id });
await this.collection.deleteOne({ _id: ObjectId(id) });
delete before._id;
return before;
}
}
module.exports = {
ProductService
};
Note: In the same way, as in the PersonService I’m passing the fastify app by the constructor and then I use it to get the mongo connector.
./src/environment.js:
이것은 컨텍스트(로컬, 개발, 스테이징, 프로덕션)에 따라 환경 변수를 처리하는 데 사용하는 파일이며 다음과 같습니다.
const dotenv = require('dotenv');
const path = require('path');
dotenv.config({ path: path.resolve(__dirname, '../.env') });
let envPath;
// validate the NODE_ENV
const NODE_ENV = process.env.NODE_ENV;
switch (NODE_ENV) {
case 'development':
envPath = path.resolve(__dirname, '../.env.development');
break;
case 'staging':
envPath = path.resolve(__dirname, '../.env.staging');
break;
case 'production':
envPath = path.resolve(__dirname, '../.env.production');
break;
default:
envPath = path.resolve(__dirname, '../.env.local');
break;
};
dotenv.config({ path: envPath });
const enviroment = {
/* GENERAL */
NODE_ENV,
TIME_ZONE: process.env.TIME_ZONE,
APP_PORT: process.env.APP_PORT || 8080,
/* DATABASE INFORMATION */
DB_NOSQL_HOST: process.env.DB_NOSQL_HOST,
DB_NOSQL_USER: process.env.DB_NOSQL_USER,
DB_NOSQL_PASSWORD: process.env.DB_NOSQL_PASSWORD,
DB_NOSQL_NAME: process.env.DB_NOSQL_NAME,
DB_NOSQL_PORT: process.env.DB_NOSQL_PORT,
DB_SQL_CLIENT: process.env.DB_SQL_CLIENT,
DB_SQL_HOST: process.env.DB_SQL_HOST,
DB_SQL_USER: process.env.DB_SQL_USER,
DB_SQL_PASSWORD: process.env.DB_SQL_PASSWORD,
DB_SQL_NAME: process.env.DB_SQL_NAME,
DB_SQL_PORT: process.env.DB_SQL_PORT
};
module.exports = enviroment;
./src/app.js:
이제 우리는 이 API 데모에서 가장 중요한 파일에 있습니다. 이 파일에는 프로젝트의 구성/설정이 있습니다.
const Fastify = require('fastify');
const cors = require('cors');
// order to register / load
// 1. plugins (from the Fastify ecosystem)
// 2. your plugins (your custom plugins)
// 3. decorators
// 4. hooks and middlewares
// 5. your services
const build = async () => {
const fastify = Fastify({
bodyLimit: 1048576 * 2,
logger: { prettyPrint: true }
});
// plugins
await require('./plugins/mongo-db-connector')(fastify);
await fastify.register(require('fastify-express'));
await fastify.register(require('./plugins/knex-db-connector'), {});
await fastify.register(require('./routes/api'), { prefix: 'api' });
// hooks
fastify.addHook('onClose', (instance, done) => {
const { knex } = instance;
knex.destroy(() => instance.log.info('knex pool destroyed.'));
});
// middlewares
fastify.use(cors());
return fastify;
};
// implement inversion of control to make the code testable
module.exports = {
build
};
./src/start.js:
이것은 서버를 시작하는 데 사용하는 파일입니다.
const { build } = require('./app');
const { APP_PORT } = require('./environment');
build()
.then(app => {
// run the server!
app.listen(APP_PORT, (err, address) => {
if (err) {
app.log.error(err);
process.exit(1);
}
app.log.info(`server listening on ${address}`);
process.on('SIGINT', () => app.close());
process.on('SIGTERM', () => app.close());
});
});
프로젝트가 콘솔에서 다음과 같이 표시됩니다.
이것이 swagger UI 문서의 모습입니다.
끝내자
그게 다야, 질문이나 피드백이 있으면 누군가가 새로운 것을 배우는 데 도움이 되기를 바랍니다. 읽어주셔서 감사합니다.
다른 사람들을 돕기 위해 더 많은 포스트 출판물을 만들고 싶습니다.
Reference
이 문제에 관하여(fastify를 사용하는 데모 API), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/cristiandi/demo-api-using-fastify-48jo텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)