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 isapplication/json
. If its value istext/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:
- Perfect match:
/
cat
- Path parameter match:
/
cat
/:catIndex
- Wildcards:
/
cat
/*
- 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.