を使用したデモAPI


導入


私は新しい技術と新しい良いものを学ぶことを探していました.
一見すると、それは表現に非常に類似したフレームワークです、しかし、それはより良い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クエリビルダを使用しています.私にとっては、それは非常に細かいです.私はちょうど製品のための1つの移行ファイルを持っています(20200124230315 ChrateRound Persononal 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/plugins :
Fastifyで重要な側面は、プラグインの概念です、それはコード機能をカプセル化するのに役立つし、Fastifyアプリオブジェクトでそれらを使用します.私は、KONEX接続とMongo接続のための1つのために2つのプラグインを持っています.

  • /src/plugins/knex DBコネクタ.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コネクタ.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/route :
    Expressでのように、FASTIFYのルートはリクエストオブジェクトとレスポンスオブジェクト(FASTIFFYの応答)を持つルートハンドラーを持っていますが、FASTIFYでは、ルートはJSON Schemaを使用して検証とシリアル化のような他の機能を持つことができます.

  • /src/route/api/indexjs
  • 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/route/api/person/indexjs
  • 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/route/api/person/schemajs
  •    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/route/API/products/indexjs
  • 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/route/API/products/schemajs
  • 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/route/docs/indexjs
  • 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/services :
    このフォルダには、エンドポイントから必要なすべてのロジックを解決するための“クラス”が含まれます.

  • /src/services/personjs
  • 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/productsjs
  • 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/環境js
    これはコンテキストに依存して環境変数VARSを扱うためのファイルです(ローカル、開発、ステージング、プロダクション).
    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/appjs
    今、私たちはこの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/startjs
    これは私がサーバーを起動するために使用するファイルです.
     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ドキュメントがどのように見えるかです.

    終わりましょう


    これはすべて、私は誰かがいくつかの質問やフィードバックをしてくださいコメントをしてください、何か新しいことを学ぶことができます願っています.
    私は、他の人を助けるために試みるポスト出版物を作りたいです.