Fastify Firestore Service

This is a backend web framework built on Fastify and our Firestore ORM. It provides a way to define APIs, including managing data.

JSDoc

Topics

Key Features

Getting Started

Creating An API

You can create an API to fetch information from Firestore like this:

import { DatabaseAPI, EXCEPTIONS } from '@pbvision/fastify-firestore-service'

class GetOrderAPI extends DatabaseAPI {
  static PATH = '/getOrder'
  static DESC = `Get an order by ID, if order doesn't exists a 404 Not found
    error is returned`

  static INPUT = Order.KEY
  static OUTPUT = {
    order: Order.Schema
  }
  static ERRORS = {
    EXCEPTIONS.NotFoundException
  }

  async computeResponse ({ tx, body }) {
    const order = await tx.get(Order, body.id)
    if (!order) {
      throw new NotFoundException()
    }
    return { order: order.toJSON() }
  }
}

You can read more about the API interface here.

Creating a Service

A service is just a server which hosts some HTTP APIs. To create a service, you call makeService like this:

import { makeService } from '@pbvision/fastify-firestore-service'

const components = {
  Order,
  GetOrderAPI
}
export default async (params) => makeService({
  service: 'unittest',
  components,
  cookie: {
    secret: 'unit-test'
  },
  logging: {
    reportErrorDetail: true, // process.env.NODE_ENV === 'localhost',
    reportAllErrors: true // process.env.NODE_ENV !== 'prod'
  },
  swagger: {
    disabled: false,
    authHeaders: ['x-app', 'x-uid'],
    servers: ['http://localhost:8080'],
    routePrefix: '/app/docs'
  },
  ...(params ?? {})
})

Running a Server

The makeService() helper method creates a fastify instance with a few plugins loaded already. You may customize the fastify instance further using fastify's customization features. To start the app, you have to call .listen() according to Fastify's documentation. For example, makeService() is called in a app.js file, and the returned promise is exported, then you write the following code to start a server:

const app = await makeTestApp(params)
app.listen({ port: 8090, host: '0.0.0.0' })

Setting up Error Reporting

Any 500 HTTP response will be logged to Sentry if:

  • The NODE_ENV environment variable is not localhost
  • The service's logging configuration has the sentryDSN parameter set

Deliberate client errors -- a RequestError (or subclass) with HTTP status < 500 -- are expected outcomes and are NOT reported to Sentry (they are still logged and returned to the caller as usual). To report one anyway, flag it at the throw site with throw new BadRequestException('...').forceSentry(). Any error NOT thrown through the exception classes is always reported, even if it carries a 4xx statusCode (e.g., a third-party HTTP client error that escaped uncaught), so unexpected errors can never be silently dropped. Errors that fire in bursts (e.g., during a dependency outage) can be throttled with .rateLimitSentry(windowMs).

The Sentry repo will include:

  • Information about the user like ID or IP, as well as their user agent header
  • Information about the request including HTTP method, URL and response code
  • A unique ID for the request (the first part is a UUID identifying the fastify instance which is running, and the second part is a number indicating this requests unique number on this instance)
  • Stack trace of the error
  • Environment name (e.g., test or prod)
  • Release = git hash of the source code for the service that's running

Components

A service is composed of components. A component can be an API, a DB Model, etc. These components are passed to makeService() which calls the register() method on each component so that it can do any setup it requires:

const components = {
  Order,
  GetOrderAPI
}

makeService({
  components,
  ...
})

For example, API's register with fastify as a route.

Customizing Component Registration

The component system uses a visitor pattern to allow extending the registration workflow with custom components. For example, to add a new type of component ExampleComponent, you need to do the following:

  1. Subclass ComponentRegistrar, and add a registerExampleComponent (exampleComponent) method
    import { ComponentRegistrar } from '@pbvision/fastify-firestore-service'
    
    class CustomComponentRegistrar extends ComponentRegistrar {
        registerExampleComponent (exampleComponent) {
            // do what needs to be done
        }
    }
    
  2. You can pass the new CustomComponentRegistrar class to makeService() like this
    makeService({
        RegistrarCls: CustomComponentRegistrar
    })
    
  3. Implement static register (registrar) in the new ExampleComponent class
    class ExampleComponent {
        static register (registrar) {
            registrar.registerExampleComponent(this)
        }
    }
    
  4. Pass the new type of component as part of components like this makeService({ components: { ExampleComponent } })

Unit testing

Generating SDKs

Swagger UI

This library generates an interactive Swagger UI for all APIs at /[service]/docs.

OpenAPI SDKs

You can export APIs in an OpenAPI schema from /[service]/docs/json, and use that with OpenAPI / Swagger SDK to generate SDKs in any supported languages.

CAUTION: Swagger SDKs use positional arguments in all SDKs, maintaining backward compatibility will be challenging with vanilla SDK generators. You may customize the generators to pass keyword arguments instead for languages that support it.

Generated from 35522fb49549382afda2a2fa0e1b8b78d1bc275c