发布于 2026-01-06 0 阅读
0

使用 fastify DEV 的全球展示与分享挑战赛演示 API,由 Mux 呈现:展示你的项目!

使用 fastify 的演示 API

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

介绍

我一直在寻找新技术和值得学习的新知识,就这样了解到了 Fastify。
乍一看,它和 Express 框架非常相似,但它的性能测试结果更好,而且还有一些相当不错的功能,所以我决定试用一下,这就是这篇文章的由来。

本文已更新,代码现在可在 Fastify 3.x 版本上运行。

本文所有代码都在这里

我们开始吧

项目结构:

./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
Enter fullscreen mode Exit fullscreen mode

./migrations:
这里我使用的是Knex查询构建器,对我来说非常好用,处理迁移和种子数据也很容易。我只有一个用于Product 表的迁移文件 (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
};
Enter fullscreen mode Exit fullscreen mode

注意:要快速了解迁移或种子,您可以查看此概要

./src/plugins:Fastify
的一个重要方面是插件概念,它有助于封装代码功能,然后在 Fastify 应用对象中使用它们。我有两个插件,一个用于 Knex 连接,另一个用于 MongoDB 连接

  • ./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);
Enter fullscreen mode Exit fullscreen mode
  • ./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;
Enter fullscreen mode Exit fullscreen mode

注意:这里我使用了fastify-mongodb来辅助连接。目前它抛出了一个关于某些已弃用方法的警告,这个问题将在fastify-mongodb的后续更新中得到解决。

./src/routes
与 Express 类似,fastify 中的路由有一个路由处理程序,其中包含请求对象和响应对象(fastify 中的 reply),但在 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;
Enter fullscreen mode Exit fullscreen mode

注意:如您所见,我有一个 GET 路由 '/',并且它有一个路由处理程序,我还将其他路由用作人员产品端点的插件,我在下面描述它们,最后我使用了fastify-swagger,别担心,我会解释的。

  • ./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;
Enter fullscreen mode Exit fullscreen mode

注意:这里有所有与人员相关的端点,我为每个端点使用了架构,并且我还使用了PersonService,我稍后会解释。

  • ./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
};
Enter fullscreen mode Exit fullscreen mode

注意:以下是每个端点的所有模式,使用这些模式我可以进行一些重要的验证,例如,如果 body 属性被定义为数字,则它不能是字符串;字符串属性的最大长度等等。

  • ./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;
Enter fullscreen mode Exit fullscreen mode
  • ./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
};
Enter fullscreen mode Exit fullscreen mode
  • ./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' }
    ]
  }
};
Enter fullscreen mode Exit fullscreen mode

注意:接下来要谈谈fastify-swagger,它是一个模块,可以帮助我们使用此“配置文件”和之前使用的模式为每个端点创建 API 文档。

./src/services
此文件夹包含负责解析端点所需所有逻辑的“类”。

  • ./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
};
Enter fullscreen mode Exit fullscreen mode

注意:正如您所看到的,这是我在人员路由中使用的服务,通过构造函数我传递了 fastify 应用程序,然后从 fastify 应用程序获取 knex 连接器以与数据库保持联系。

  • ./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
};
Enter fullscreen mode Exit fullscreen mode

注意:与PersonService类似,我通过构造函数传递 fastify 应用程序,然后使用它来获取 mongo 连接器。

./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;
Enter fullscreen mode Exit fullscreen mode

./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
};
Enter fullscreen mode Exit fullscreen mode

./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());
    });
  });

Enter fullscreen mode Exit fullscreen mode

这是项目在控制台上的显示效果:
替代文字

这就是 Swagger UI 文档的样子:
替代文字

我们结束吧

以上就是全部内容,希望对大家有所帮助。如果您有任何问题或反馈,请留言。感谢阅读。
我希望能发布更多文章,帮助更多的人。

文章来源:https://dev.to/cristiandi/demo-api-using-fastify-48jo