Demo API using fastify
Tags: javascript, node, knex, fastify
Published: 2020-01-26 21:01:44


## Introduction
I was looking for new technologies and new good things to learn and that's how I get to know about fastify.
At first look, it's a framework quite similar to Express, but, it has better [benchmarks](https://github.com/fastify/fastify#benchmarks) and some pretty good features, so I decided to try it and that's why this post.

***This post was updated and the code now its working with fastify 3.x***

All code about this post is [here](https://github.com/Cristiandi/demo-fastify).

## let's start
*Project structure:*
```
./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
```
**./migrations:**
In this case I'm using the [knex](https://knexjs.org/) query builder, for me it’s quite fine and working with migrations and seeds is easy. I just have one migration file for the *Product (20200124230315_create_Person_table.js)* table and looks like:
```javascript
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](https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261).

**./src/plugins**:
An important aspect at fastify is the plugin concept, it’s helpful to encapsulate code functionalities and then use them at fastify app object. I have two plugins one for knex connection and one for mongo connection.
- ***./src/plugins/knex-db-connector.js***:
```javascript
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***:
```javascript
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/routes**:
Like in Express, the routes in fastify have a route handler with the request object and the response object (reply for fastify), but in fastify, the routes can have some other features like validation and serialization using [JSON Schema](https://json-schema.org/).

- ***./src/routes/api/index.js***:
```javascript
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***:
```javascript
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***:
```javascript
   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***:
```javascript
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***:
```javascript
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***:
```javascript
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*](https://github.com/fastify/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**:
This folder contains the “classes” in charge to resolve all the logic needed from the endpoints.

- ***./src/services/persons.js***:
```javascript
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***:
```javascript
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***:
This is a file that I use to handle the environment vars depending on the context (*local, development, staging, production*), and looks like:
```javascript
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***:
Now we are in the most important file in this API demo, this file has the config/setup of the project.

```javascript
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***:
 This is the file that I use to start the server.
 ```javascript
 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());
    });
  });

 ```
 
This is how the project looks like on the console:
![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/mjqzbqpc2dk7dq2gp23j.png)

This is what the swagger ui documentation looks like:
![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/9f0vhzpades3x4kebz1v.png)

## let's finish
That’s all, I hope this helps somebody to learn something new if you have some questions or feedbacks please comment, thank you for reading.
I would like to make more post publications to try to help others.