Book Image

Accelerating Server-Side Development with Fastify

By : Manuel Spigolon, Maksim Sinik, Matteo Collina
5 (1)
Book Image

Accelerating Server-Side Development with Fastify

5 (1)
By: Manuel Spigolon, Maksim Sinik, Matteo Collina

Overview of this book

This book is a complete guide to server-side app development in Fastify, written by the core contributors of this highly performant plugin-based web framework. Throughout the book, you’ll discover how it fosters code reuse, thereby improving your time to market. Starting with an introduction to Fastify’s fundamental concepts, this guide will lead you through the development of a real-world project while providing in-depth explanations of advanced topics to prepare you to build highly maintainable and scalable backend applications. The book offers comprehensive guidance on how to design, develop, and deploy RESTful applications, including detailed instructions for building reusable components that can be leveraged across multiple projects. The book presents guidelines for creating efficient, reliable, and easy-to-maintain real-world applications. It also offers practical advice on best practices, design patterns, and how to avoid common pitfalls encountered by developers while building backend applications. By following these guidelines and recommendations, you’ll be able to confidently design, implement, deploy, and maintain an application written in Fastify, and develop plugins and APIs to contribute to the Fastify and open source communities.
Table of Contents (21 chapters)
1
Part 1:Fastify Basics
7
Part 2:Build a Real-World Project
14
Part 3:Advanced Topics

Adding basic routes

The routes are the entry to our business logic. The HTTP server exists only to manage and expose routes to clients. A route is commonly identified by the HTTP method and the URL. This tuple matches your function handler implementation. When a client hits the route with an HTTP request, the function handler is executed.

We are ready to add the first routes to our playground application. Before the listen call, we can write the following:

app.route({
  url: '/hello',
  method: 'GET',
  handler: function myHandler(request, reply) {
    reply.send('world')
  }
})

The route method accepts a JavaScript object as a parameter to set the HTTP request handler and the endpoint coordinates. This code will add a GET /hello endpoint that will run the myHandler function whenever an HTTP request matches the HTTP method and the URL that was just set. The handler should implement the business logic of your endpoint, reading from the request component and returning a response to the client via the reply object.

Note that running the previous code in your source code must trigger the onRoute hook that was sleeping before; now, the http://localhost:8080/hello URL should reply, and we finally have our first endpoint!

Does the onRoute hook not work?

If the onRoute hook doesn’t show anything on the terminal console, remember that the addRoute method must be called after the addHook function! You have spotted the nature a hook may have: the application’s hooks are synchronous and are triggered as an event happens, so the order of the code matters for these kinds of hooks. This topic will be broadly discussed in Chapter 4.

When a request comes into the Fastify server, the framework takes care of the routing. It acts by default, processing the HTTP method and the URL from the client, and it searches for the correct handler to execute. When the router finds a matching endpoint, the request lifecycle will start running. Should there be no match, the default 404 handler will process the request.

You have seen how smooth adding new routes is, but can it be even smoother? Yes, it can!

Shorthand declaration

The HTTP method, the URL, and the handler are mandatory parameters to define new endpoints. To give you a less verbose routes declaration, Fastify supports three different shorthand syntaxes:

app.get(url, handlerFunction) // [1]
app.get(url, { // [2]
  handler: handlerFunction,
  // other options
})
app.get(url, [options], handlerFunction) // [3]

The first shorthand [1] is the most minimal because it accepts an input string as a URL and handler. The second shorthand syntax [2] with options will expect a string URL and a JavaScript object as input with a handler key with a function value. The last one [3] mixes the previous two syntaxes and lets you provide the string URL, route options, and function handler separately: this will be useful for those routes that share the same options but not the same handler!

All the HTTP methods, including GET, POST, PUT, HEAD, DELETE, OPTIONS, and PATCH, support this declaration. You need to call the correlated function accordingly: app.post(), app.put(), app.head(), and so on.

The handler

The route handler is the function that must implement the endpoint business logic. Fastify will provide your handlers with all its main components, in order to serve the client’s request. The request and reply object components will be provided as arguments, and provide the server instance through the function binding:

function business(request, reply) {
  // `this` is the Fastify application instance
  reply.send({ helloFrom: this.server.address() })
}
app.get('/server', business)

Using an arrow function will prevent you from getting the function context. Without the context, you don’t have the possibility to use the this keyword to access the application instance. The arrow function syntax may not be a good choice because it can cause you to lose a great non-functional feature: the source code organization! The following handler will throw a Cannot read property 'server' of undefined error:

app.get('/fail', (request, reply) => {
  // `this` is undefined
  reply.send({ helloFrom: this.server.address() })
})

Context tip

It would be best to choose named functions. In fact, avoiding arrow function handlers will help you debug your application and split the code into smaller files without carrying boring stuff, such as the application instance and logging objects. This will let you write shorter code and make it faster to implement new endpoints. The context binding doesn’t work exclusively on handlers but also works on every Fastify input function and hook, for example!

The business logic can be synchronous or asynchronous: Fastify supports both interfaces, but you must be aware of how to manage the reply object in your source code. In both situations, the handler should never call reply.send(payload)more than once. If this happens, it will work just for the first call, while the subsequent call will be ignored without blocking the code execution:

app.get('/multi', function multi(request, reply) {
  reply.send('one')
  reply.send('two')
  reply.send('three')
  this.log.info('this line is executed')
})

The preceding handler will reply with the one string, and the next reply.send calls will log an FST_ERR_REP_ALREADY_SENT error in the console.

To ease this task, Fastify supports the return even in the synchronous function handler. So, we will be able to rewrite our first section example as the following:

function business(request, reply) {
  return { helloFrom: this.server.address() }
}

Thanks to this supported interface, you will not mess up multiple reply.send calls!

The async handler function may completely avoid calling the reply.send method instead. It can return the payload directly. We can update the GET /hello endpoint to this:

app.get('/hello', async function myHandler(request, reply) {
  return 'hello' // simple returns of a payload
})

This change will not modify the output of the original endpoint: we have updated a synchronous interface to an async interface, updating how we manage the response payload accordingly. The async functions that do not execute the send method can be beneficial to reuse handlers in other handler functions, as in the following example:

async function foo (request, reply) {
  return { one: 1 }
}
async function bar (request, reply) {
  const oneResponse = await foo(request, reply)
  return {
    one: oneResponse,
    two: 2
  }
}
app.get('/foo', foo)
app.get('/bar', bar)

As you can see, we have defined two named functions: foo and bar. The bar handler executes the foo function and it uses the returned object to create a new response payload.

Avoiding the reply object and returning the response payload unlocks new possibilities to reuse your handler functions, because calling the reply.send() method would explicitly prevent manipulating the results as the bar handler does.

Note that a sync function may return a Promise chain. In this case, Fastify will manage it like an async function! Look at this handler, which will return file content:

const fs = require('fs/promises')
app.get('/file', function promiseHandler(request, reply) {
  const fileName = './package.json'
  const readPromise = fs.readFile(fileName, { encoding:
  'utf8' })
  return readPromise
})

In this example, the handler is a sync function that returns readPromise:Promise. Fastify will wait for its execution and reply to the HTTP request with the payload returned by the promise chain. Choosing the async function syntax or the sync and Promise one depends on the output. If the content returned by the Promise is what you need, you can avoid adding an extra async function wrapper, because that will slow down your handler execution.

The Reply component

We have already met the Reply object component. It forwards the response to the client, and it exposes all you need in order to provide a complete answer to the request. It provides a full set of functions to control all response aspects:

  • reply.send(payload) will send the response payload to the client. The payload can be a String, a JSON object, a Buffer, a Stream, or an Error object. It can be replaced by returning the response’s body in the handler’s function.
  • reply.code(number) will set the response status code.
  • reply.header(key, value) will add a response header.
  • reply.type(string) is a shorthand to define the Content-Type header.

The Reply component’s methods can be chained to a single statement to reduce the code noise as follows: reply.code(201).send('done').

Another utility of the Reply component is the headers’ auto-sense. Content-Length is equal to the length of the output payload unless you set it manually. Content-Type resolves strings to text/plain, a JSON object to application/json, and a stream or a buffer to the application/octet-stream value. Furthermore, the HTTP return status is 200 Successful when the request is completed, whereas when an error is thrown, 500 Internal Server Error will be set.

If you send a Class object, Fastify will try to call payload.toJSON() to create an output payload:

class Car {
  constructor(model) {
    this.model = model
  }
  toJSON() {
    return {
      type: 'car',
      model: this.model
    }
  }
}
app.get('/car', function (request, reply) {
  return new Car('Ferrari')
})

Sending a response back with a new Car instance to the client would result in the JSON output returned by the toJSON function implemented by the class itself. This is useful to know if you use patterns such as Model View Controller (MVC) or Object Relational Mapping (ORM) extensively.

The first POST route

So far, we have seen only HTTP GET examples to retrieve data from the backend. To submit data from the client to the server, we must switch to the POST HTTP method. Fastify helps us read the client’s input because the JSON input and output is a first-class citizen, and to process it, we only need to access the Request component received as the handler’s argument:

const cats = []
app.post('/cat', function saveCat(request, reply) {
  cats.push(request.body)
  reply.code(201).send({ allCats: cats })
})

This code will store the request body payload in an in-memory array and send it back as a result.

Calling the POST /cat endpoint with your HTTP client will be enough to parse the request’s payload and reply with a valid JSON response! Here is a simple request example made with curl:

$ curl --request POST "http://127.0.0.1:8080/cat" --header "Content-Type: application/json" --data-raw "{\"name\":\"Fluffy\"}"

The command will submit the Fluffy cat to our endpoint, which will parse the payload and store it in the cats array.

To accomplish this task, you just have to access the Request component without dealing with any complex configuration or external module installation! Now, let’s explore in depth the Request object and what it offers out of the box.

The Request component

During the implementation of the POST route, we read the request.body property. The body is one of the most used keys to access the HTTP request data. You have access to the other piece of the request through the API:

  • request.query returns a key-value JavaScript object with all the query-string input parameters.
  • request.params maps the URL path parameters to a JavaScript object.
  • request.headers maps the request’s headers to a JavaScript object as well.
  • request.body returns the request’s body payload. It will be a JavaScript object if the request’s Content-Type header is application/json. If its value is text/plain, the body value will be a string. In other cases, you will need to create a parser to read the request payload accordingly.

The Request component is capable of reading information about the client and the routing process too:

app.get('/xray', function xRay(request, reply) {
  // send back all the request properties
  return {
    id: request.id, // id assigned to the request in req-
                       <progress>
    ip: request.ip, // the client ip address
    ips: request.ips, // proxy ip addressed
    hostname: request.hostname, // the client hostname
    protocol: request.protocol, // the request protocol
    method: request.method, // the request HTTP method
    url: request.url, // the request URL
    routerPath: request.routerPath, // the generic handler
                                       URL
    is404: request.is404 // the request has been routed or
                            not
  }
})

request.id is a string identifier with the "req-<progression number>" format that Fastify assigns to each request. The progression number restarts from 1 at every server restart. The ID’s purpose is to connect all the logs that belong to a request:

app.get('/log', function log(request, reply) {
  request.log.info('hello') // [1]
  request.log.info('world')
  reply.log.info('late to the party') // same as
                                         request.log
  app.log.info('unrelated') // [2]
  reply.send()
})

Making a request to the GET /log endpoint will print out to the console six logs:

  • Two logs from Fastify’s default configuration that will trace the incoming request and define the response time
  • Four logs previously written in the handler

The output should be as follows:

{"level":30,"time":1621781167970,"pid":7148,"hostname":"EOMM-XPS","reqId":"req-1","req":{"method":"GET","url":"/log","hostname":"localhost:8080","remoteAddress":"127.0.0.1","remotePort":63761},"msg":"incoming request"}
{"level":30,"time":1621781167976,"pid":7148,"hostname":"EOMM-XPS","reqId":"req-1","msg":"hello"}
{"level":30,"time":1621781167977,"pid":7148,"hostname":"EOMM-XPS","reqId":"req-1","msg":"world"}
{"level":30,"time":1621781167978,"pid":7148,"hostname":"EOMM-XPS","reqId":"req-1","msg":"late to the party"}
{"level":30,"time":1621781167979,"pid":7148,"hostname":"EOMM-XPS","msg":"unrelated"}
{"level":30,"time":1621781167991,"pid":7148,"hostname":"EOMM-XPS","reqId":"req-1","res":{"statusCode":200},"responseTime":17.831200003623962,"msg":"request completed"}

Please note that only the request.log and reply.log commands [1] have the reqId field, while the application logger doesn’t [2].

The request ID feature can be customized via these server options if it doesn’t fit your system environment:

const app = fastify({
  logger: true,
  disableRequestLogging: true, // [1]
  requestIdLogLabel: 'reqId', // [2]
  requestIdHeader: 'request-id', // [3]
  genReqId: function (httpIncomingMessage) { // [4]
  return `foo-${Math.random()}`
  }
})

By turning off the request and response logging [1], you will take ownership of tracing the clients’ calls. The [2] parameter customizes the field name printed out in the logs, and [3] informs Fastify to obtain the ID to be assigned to the incoming request from a specific HTTP header. When the header doesn’t provide an ID, the genReqId function [4] must generate a new ID.

The default log output format is a JSON string designed to be consumed by external software to let you analyze the data. This is not true in a development environment, so to see a human-readable output, you need to install a new module in the project:

npm install pino-pretty –-save-dev

Then, update your logger settings, like so:

const serverOptions = {
  logger: {
    level: 'debug',
    transport: {
      target: 'pino-pretty'
    }  }
}

Restarting the server with this new configuration will instantly show a nicer output to read. The logger configuration is provided by pino. Pino is an external module that provides the default logging feature to Fastify. We will explore this module too in Chapter 11.

Parametric routes

To set a path parameter, we must write a special URL syntax, using the colon before our parameter’s name. Let’s add a GET endpoint beside our previous POST /cat route:

app.get('/cat/:catName', function readCat(request, reply) {
  const lookingFor = request.params.catName
  const result = cats.find(cat => cat.name == lookingFor)
  if (result) {
    return { cat: result }
  } else {
    reply.code(404)
    throw new Error(`cat ${lookingFor} not found`)
  }
})

This syntax supports regular expressions too. For example, if you want to modify the route previously created to exclusively accept a numeric parameter, you have to write the RegExp string at the end of the parameter’s name between parentheses:

app.get('/cat/:catIndex(\\d+)', function readCat(request,
reply) {
  const lookingFor = request.params.catIndex
  const result = cats[lookingFor]
  // …
})

Adding the regular expression to the parameter name will force the router to evaluate it to find the right route match. In this case, only when catIndex is a number will the handler be executed; otherwise, the 404 fallback will take care of the request.

Regular expression pitfall

Don’t abuse the regular expression syntax in the path parameters because it comes with a performance cost. Moreover, a mismatch of regular expressions will lead to a 404 response. You may find it useful to validate the parameter with the Fastify validator, which we present in Chapter 5 to reply with a 400 Bad Request status code.

The Fastify router supports the wildcard syntax too. It can be useful to redirect a root path or to reply to a set of routes with the same handler:

app.get('/cat/*', function sendCats(request, reply) {
  reply.send({ allCats: cats })
})

Note that this endpoint will not conflict with the previous because they are not overlapping, thanks to the match order:

  1. Perfect match: /cat
  2. Path parameter match: /cat/:catIndex
  3. Wildcards: /cat/*
  4. Path parameter with a regular expression: /cat/:catIndex(\\d+)

Under the hood, Fastify uses the find-my-way package to route the HTTP request, and you can benefit from its features.

This section explored how to add new routes to our application and how many utilities Fastify gives us, from application logging to user input parsing. Moreover, we covered the high flexibility of the reply object and how it supports us when returning complex JSON to the client. We are now ready to go further and start understanding Fastify plugin system basics.