|||

Video Transcript

X

Build Well-Documented and Authenticated APIs in Node.js with Fastify

If you’re an API developer working with Node.js, then you’re probably familiar with Express. But have you tried out the Fastify framework to build with power, speed, and convenience? In this walkthrough, we build a full-featured, easy-to-consume API with Fastify. And we deploy it to the cloud with ease. We show you how to:

  • Get started working with Fastify to build an API
  • Implement API authentication by using a JSON web token (JWT)
  • Use Fastify’s Swagger plugins to generate an OpenAPI specification
  • Consume the OpenAPI specification with Postman, giving you an API client that can send requests seamlessly to your back-end API
  • Deploy your application to Heroku

This project is part of our Heroku Reference Applications GitHub organization where we host different projects showcasing architectures and patterns to deploy to Heroku.

Key Concepts

Before we code, let’s briefly cover the core concepts and technologies for this walkthrough.

Application Flow

A Heroku Postgres database stores records of usernames, first names, last names, and emails in a users table. The public endpoint of our API (/directory) returns a list of usernames for all users in the table. The protected endpoint (/profile) requires a JWT with username in the payload. This endpoint returns additional information about the user with the given username.

Architecture diagram

What's Fastify?

Fastify is a web framework for Node.js that boasts speed, low overhead, and a delightful developer experience. Many Node.js developers have adopted Fastify as an alternative to Express.

Fastify is designed with a plugin architecture, making it incredibly modular. Its documentation says that “in Fastify everything is a plugin.” This architecture makes it easy for developers to build and use utilities, middleware, and other niceties. We dive deeper into working with plugins as we get to coding.

Authentication

Our authenticated route requires a JWT signed with an RSA256 private key. We attach that JWT, and the API uses the symmetric public key to validate it.

The username in the payload of the validated JWT is meant to represent the user making the request, so the /profile endpoint returns account information about that user.

API Documentation

We also document our API routes as we write our code. Fastify has OpenAPI support through its plugin ecosystem that generates the full OpenAPI specification and gives us a UI. With the OpenAPI specification generated, we can also use Postman to import the spec to give us a client that can send requests to our API.

Deployment

After doing a little bit of local testing, we can deploy our API to Heroku with just a few quick CLI commands, or the Deploy to Heroku button in the GitHub repository

Get Started

To use this demo, you need:

  1. A Heroku account. You must add a payment method to cover your compute and database costs. To run this API, go with the Eco dyno, which has a $5 monthly flat fee. You also need a Heroku Postgres instance. Go with the Mini plan, at a max of $5 monthly cost. The Eco and Mini plans are enough for this sample application.
  2. A GitHub account for your code repository. Heroku hooks into your GitHub repo directly, simplifying deployment to a single click.
  3. (Optional) The Postman client application installed on your local machine. You need Postman to follow along in our section on importing an OpenAPI specification.

You can start by cloning the GitHub repo for this project. If you simply want to deploy and start using the API, follow the instructions in the README.

To keep this walkthrough simple, we’re going to highlight the most important parts of the code to help you understand how we built this API. We don’t go through everything line by line, but you can always reference the repo codebase to examine the code itself.

Initialize the Project

When building this project, we used Node v20.11.1 along with npm as our package manager. Start by initializing a new project and installing dependencies:

npm init -y

npm install fastify fastify-cli fastify-plugin @fastify/auth @fastify/autoload @fastify/jwt @fastify/swagger @fastify/swagger-ui fast-jwt dotenv pg

Create the Initial app.js File

Just to start things out, we begin with an app.js file in our project root folder. This file is our “hello world” initial application:

app.js

export default async (fastify, opts) => {
  fastify.get(
    "/",
    async function (_request, reply) {
      reply.code(200).type("text/plan").send("hello world");
    },
  );
}

We use the fastify-cli to run the app.js file. Notice that we don’t need to import Fastify in our file, since we pass an instance of a Fastify server object, fastify, to the function as an argument. To start, we add handling for a GET request to /. As we build up our API, we can simply enhance this instance by registering new plugins.

Let’s add some lines to our package.json file to use that app.js file.

package.json

{
  "name": "openapi-fastify-jwt",
  "version": "1.0.0",
  "type": "module",
  "description": "A sample Fastify API with RSA256 JWT authentication",
  "main": "app.js",
  "scripts": {
    "start": "fastify start -a 0.0.0.0 -l info app.js",
    "dev": "fastify start -w -l info -P app.js"
  },

The fastify-cli command in our scripts section starts up our server to listen for requests. We start our local server like this:

npm run dev
[10:22:17.323] INFO (816073): Server listening at http://127.0.0.1:3000

In a separate terminal window, we test our server:

curl localhost:3000
hello world

Create the Database Plugin

Next, we write a plugin for querying our Postgres database, and add it to our fastify instance.

In a subfolder called plugins, we create a file called db.js with the following contents:

plugins/db.js

import fp from "fastify-plugin";
import pg from "pg";

const { Pool } = pg;

export default fp(async (fastify) => {
  const pool = new Pool({
    connectionString: process.env.DATABASE_URL,
    ssl: {
      rejectUnauthorized: false,
    },
  });

  fastify.decorate("db", {
    query: async (text, params) => {
      const result = await pool.query(text, params);
      return result.rows;
    },
  });
})

The standard convention for creating Fastify plugins uses the fastify-plugin package,imported above as a function called fp. We define how to enhance our fastify instance, then call fp() on that functionality and export it.

Note: The Fastify ecosystem has its own @fastify/postgresql plugin which is the recommendation for production-based applications. We decided to build our own plugin to demonstrate how to extend Fastify with a simple plugin.

Our database plugin opens a connection to a Postgres database based on the DATABASE_URL environment variable. We have a method called query which sends the SQL query along with any parameters, returning the result.

Notice that we decorate our fastify instance with the string db, supplying the definition for our query function. By doing this, we can call fastify.db.query for any fastify instance that registered this plugin.

Back in app.js, let’s register our newly created plugin. We could call fastify.register individually on each plugin we want to register, as Fastify’s getting started guide describes. However, we use @fastify/autoload to quickly register all plugins in a given folder. Our app.js file now looks like this, after removing the GET handler for /:

app.js

import path from "path";
import AutoLoad from "@fastify/autoload";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default async (fastify, opts) => {
  fastify.register(AutoLoad, {
    dir: path.join(__dirname, "plugins"),
    options: Object.assign({}),
  });
};

By using autoload, we register any plugins found in our plugins subfolder.

Create the /directory Route

Next, we add our /directory route. This public route returns all the usernames in our database’s users table. The handler uses our db plugin’s query method.

In a subfolder called routes, we create a file called directory.js with the following contents:

routes/directory.js

export default async function (fastify, _opts) {
  fastify.get(
    "/directory",
     async (_request, reply) => {
      const { db } = fastify;
      const rows = await db.query(
        "SELECT username FROM users ORDER by username",
      );
      const records = rows.map((r) => { username: r.username });
      reply.code(200).type("application/json").send(records);
    },
  );
}

Notice how we use the db object from our fastify instance. This code assumes that our fastify instance registered a plugin that decorates the instance with db, giving us convenient access to db.query. We handle GET requests to /directory by making the appropriate query and returning the results.

Back in app.js, we have to make sure to add this route to our fastify instance by calling fastify.register. Just like we did for our plugins subfolder, we autoload any files in our routes subfolder. Let’s also add in a call to dotenv, since we need our DATABASE_URL environment variable soon.

app.js

import "dotenv/config";
import path from "path";
import AutoLoad from "@fastify/autoload";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default async (fastify, opts) => {
  fastify.register(AutoLoad, {
    dir: path.join(__dirname, "plugins"),
    options: Object.assign({}),
  });

  fastify.register(AutoLoad, {
    dir: path.join(__dirname, "routes"),
    options: Object.assign({}),
  });
};

Set Up a Local Postgres Database

For local testing, we set up a local Postgres database. Then, we add the database’s connection string to a file called .env in the project root folder. For example:

.env

DATABASE_URL=postgres://user:password@localhost:5432/my_database

You can use files from the repository codebase (in the data subfolder) to create the database schema and seed the table with records.

psql \
        postgres://user:password@localhost:5432/my_database \
        < create_schema.sql

psql \
        postgres://user:password@localhost:5432/my_database \
        < create_records.sql

With the database plugin, public /directory route, and local database all in place, we test our server again. We start our server with npm run dev. Then, in a separate terminal window:

curl localhost:3000/directory
[{"username":"adelia.casper"},{"username":"aisha.upton"},{"username":"alfred.lindgren"},{"username":"alysha.mclaughlin"},{"username":"angie.keebler"},{"username":"antonia.gutmann"},{"username":"baron.hessel"},{"username":"bernadine.powlowski"},{"username":"carlee.abbott"},{"username":"charley.glover"},{"username":"cora.bednar"},{"username":"darryl.reynolds"},{"username":"dee.gorczany"},{"username":"dennis.koss"},{"username":"deshaun.wiza"},{"username":"devante.lakin"},{"username":"edythe.thompson"},{"username":"eldon.bahringer"},{"username":"elenor.trantow"},{"username":"elijah.hane"},{"username":"erin.haley"},{"username":"estefania.will"},{"username":"haven.rippin"},{"username":"houston.rowe"},{"username":"imani.okon"},{"username":"irma.durgan"},{"username":"jaiden.vandervort"},{"username":"jamar.maggio"},{"username":"jamir.walsh"},{"username":"jedediah.mraz"},{"username":"jett.beier"},{"username":"johnathon.hessel"},{"username":"jovan.turner"},{"username":"kade.hilpert"},{"username":"king.berge"},{"username":"laurie.marquardt"},{"username":"madge.hettinger"},{"username":"magali.terry"},{"username":"magdalena.farrell"},{"username":"marty.wunsch"},{"username":"mellie.donnelly"},{"username":"muriel.walker"},{"username":"noelia.jenkins"},{"username":"nolan.dubuque"},{"username":"otis.grady"},{"username":"rene.bins"},{"username":"rhoda.bashirian"},{"username":"rose.boehm"},{"username":"tatyana.wolf"},{"username":"zion.reichel"}]%

Excellent. Our public route and our database plugin look like they’re working. Now, it’s time to move onto authentication.

Create the Authentication Plugin

In our plugins subfolder, we create a new plugin in auth.js. It looks like this:

plugins/auth.js

import fp from "fastify-plugin";
import jwt from "@fastify/jwt";
import auth from "@fastify/auth";

export default fp(async (fastify) => {
  if (!process.env.RSA_PUBLIC_KEY_BASE_64) {
    throw new Error(
      "Environment variable `RSA_PUBLIC_KEY_BASE_64` is required",
    );
  }

  const publicKey = Buffer.from(
    process.env.RSA_PUBLIC_KEY_BASE_64,
    "base64",
  ).toString("ascii");
  if (!publicKey) {
    fastify.logger.error(
      "Public key not found. Make sure env var `RSA_PUBLIC_KEY_BASE_64` is set.",
    );
  }

  fastify.register(jwt, {
    secret: {
      public: publicKey,
    },
  });

  fastify.register(auth);

  fastify.decorate("verifyJWT", async (request, reply) => {
    try {
      await request.jwtVerify();
    } catch (err) {
      reply.send(err);
    }
  });
});

Our authentication process checks that the supplied JWT is properly signed. We verify the signature with the signer’s public key. Let’s walk through what we’re doing here step by step:

  1. Read in the publicKey from our RSA_PUBLIC_KEY_BASE_64 environment variable. The key must be in base64 format.
  2. Register the @fastify/jwt plugin, supplying the publicKey because we use the plugin in verify-only mode.
  3. Register the @fastify/auth plugin, which adds convenience utilities for attaching authentication to routes.
  4. Decorate our fastify instance with a function called verifyJWT. Our function calls the jwtVerify function in the @fastify/jwt plugin, passing it the API request. That function checks the Authorization header for a bearer token and verifies the JWT against our publicKey.

Because our app.js file already autoloads any plugins in our plugins subfolder, we don’t need to do anything else to register our new authentication plugin.

Create the Authenticated /profile Route

In our routes subfolder, we create a file called profile.js with the following contents:

routes/profiles.js

export default async function (fastify, _opts) {
  fastify.get(
    "/profile",
    {
      onRequest: [fastify.auth([fastify.verifyJWT])],
    },
    async (request, reply) => {
      const { db } = fastify;
      const sql =
        'SELECT id, username, first_name as "firstName", last_name as "lastName", email FROM users WHERE username=$1';

      const rows = await db.query(sql, [request.user.username]);
      if (rows.length) {
        reply.code(200).type("application/json").send(rows[0]);
      } else {
        reply.code(404).type("text/plain").send("Not Found");
      }
    },
  );
}

How we implement this route differs slightly from that of /directory. When calling fastify.get, we include an object with route options as the second argument, before our handler function definition. We include the onRequest option, which acts like middleware handling. When a request to /profile comes in, Fastify calls fastify.auth for authentication, passing it our decorated fastify.verifyJWT function as our authentication strategy.

For our route handler, notice that our SQL query references request.user.username. You might wonder where that came from. Do you remember how we expect the JWT payload to include a username? When the @fastify/jwt plugin verifies the JWT, it writes the JWT payload to a user object in the request, passing that payload information downstream. That gives us access to request.user.username in our route handler. We call our database plugin to query for the user’s information, and we send the response.

And, because app.js autoloads the routes subfolder, our server is immediately serving up this route.

Generating Keys and Tokens

When we deploy our API, we use a new pair of public/private RSA keys. You can generate a pair online here. You need the public key, in base64 format, as an environment variable for JWT verification. You only use the private key when signing a JWT for accessing the API’s authenticated route.

Our codebase provides a utility for generating a JWT and signing it with a private key. Here’s an example of how to use it:

npm run generate:jwt \
           utils/keys/private_key.example.rsa \
           '{"username":"aisha.upton"}'

Token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFpc2hhLnVwdG9uIiwiaWF0IjoxNzE0NDEzNzk3fQ.U0Nkb5IIDKjGv2VHFZQZE8nMpDbj25ui1b868lAnLU5T_rUcsYq-oq792gFlHcMdYmYZ92eHfqEVKjqEcKbeVRCrWSUi3pm0BN74cXZ8Q0DWc1EdxxsgtxdPZ9jtckUkeCG9BNsMBbCAQfSb_cURq4hbX9js28DYP3sVuc5soKE

With a valid token, we can test our server’s authenticated route:

# Valid token
curl \
   --header "Authorization:Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFpc2hhLnVwdG9uIiwiaWF0IjoxNzE0NDEzNzk3fQ.U0Nkb5IIDKjGv2VHFZQZE8nMpDbj25ui1b868lAnLU5T_rUcsYq-oq792gFlHcMdYmYZ92eHfqEVKjqEcKbeVRCrWSUi3pm0BN74cXZ8Q0DWc1EdxxsgtxdPZ9jtckUkeCG9BNsMBbCAQfSb_cURq4hbX9js28DYP3sVuc5soKE" \
   localhost:3000/profile
{"id":"402b11d2-20a0-4104-9800-9b5b9dee4dc1","username":"aisha.upton","firstName":"Aisha","lastName":"Upton","email":"aisha.upton@example.com"}%

Our authentication works!

Here are some examples of how the @fastify/auth and @fastify/jwt plugins handle bad requests, just to show how it looks:

# No token
curl localhost:3000/profile
{"statusCode":401,"code":"FST_JWT_NO_AUTHORIZATION_IN_HEADER","error":"Unauthorized","message":"No Authorization was found in request.headers"}

# Invalid token
curl --header "Authorization:Bearer this-is-not-a-valid-token" localhost:3000/profile

{"statusCode":401,"code":"FST_JWT_AUTHORIZATION_TOKEN_INVALID","error":"Unauthorized","message":"Authorization token is invalid: The token is malformed."}

# Token signed by a different key
curl \
   --header "Authorization:Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkNydXoxOSIsImlhdCI6MTcwOTMyMTc4Mn0.YWklNLXmojxc7Kg0M0utMHQGylsUK3LrHozvcVPYHCvZIG-nwJKKSW9FKzQ9I0glxZdWvjELGwoP7uWVGHyyEo7c3HTk1pxG-av7T9CmWf_Gk0D58n1T1PkeO7YqE-2JL6vIlvnAiUQRrrknYlEAc8Z3UruYik_CFqoRxbLkZl8" \
   localhost:3000/profile

{"statusCode":401,"code":"FST_JWT_AUTHORIZATION_TOKEN_INVALID","error":"Unauthorized","message":"Authorization token is invalid: The token signature is invalid."}

Use OpenAPI and Swagger UI for Documentation

With Fastify, we can take advantage of the @fastify/swagger and @fastify/swagger-ui plugins to conveniently generate an OpenAPI specification for our API.

First, we define our data model schemas (in schemas/index.js) using the Validation and Serialization feature from Fastify.

Next, in app.js, we register the @fastify/swagger plugin and supply it with general information about our server. We also register the @fastify/swagger-ui, providing a path (/api-docs). This plugin creates an entire Swagger UI with our OpenAPI specification at that path. Our final app.js file looks like this:

app.js

import "dotenv/config";
import path from "path";
import AutoLoad from "@fastify/autoload";
import Swagger from "@fastify/swagger";
import SwaggerUI from "@fastify/swagger-ui";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export const options = {};

export default async (fastify, opts) => {
  fastify.register(Swagger, {
    openapi: {
      info: {
        title: "User Directory and Profile",
        description:
          "Demonstrates Fastify with authenticated route using RSA256",
        version: "1.0.0",
      },
      components: {
        securitySchemes: {
          BearerAuth: {
            description:
              "RSA256 JWT signed by private key, with username in payload",
            type: "http",
            scheme: "bearer",
            bearerFormat: "JWT",
          },
        },
      },
      fastifys: [
        {
          url: "http://localhost:8080",
        },
      ],
      tags: [
        {
          name: "user",
          description: "User-related endpoints",
        },
      ],
    },
    refResolver: {
      buildLocalReference: (json, _baseUri, _fragment, _i) => {
        return json.$id || `def-{i}`;
      },
    },
  });

  fastify.register(SwaggerUI, {
    routePrefix: "/api-docs",
  });

  fastify.register(AutoLoad, {
    dir: path.join(__dirname, "plugins"),
    options: Object.assign({}),
  });

  fastify.register(AutoLoad, {
    dir: path.join(__dirname, "routes"),
    options: Object.assign({}),
  });
};

We also want to add OpenAPI specification info for each of our routes. As an example, here is how we do it in routes/profile.js:

routes/profile.js

import {
  profileSchema,
  errorSchema,
} from "../schemas/index.js";

export default async function (fastify, _opts) {
  fastify.addSchema({
    $id: "profile",
    ...profileSchema,
  });

  fastify.addSchema({
    $id: "error",
    ...errorSchema,
  });

  fastify.get(
    "/profile",
    {
      schema: {
        description:
          "Get user's own profile with additional account attributes",
        tags: ["user"],
        security: [
          {
            BearerAuth: [],
          },
        ],
        response: {
          200: {
            description: "User profile",
            $ref: "profile#",
          },
          404: {
            description: "Not Found",
            $ref: "error#",
          },
          500: {
            description: "Internal Server Error",
            $ref: "error#",
          },
        },
      },
      onRequest: [fastify.auth([fastify.verifyJWT])],
    },
    async (request, reply) => {
      …
    },
  );
}

In this file, we add a schema object to our route options argument. In line with how OpenAPI specifications are written, we add information regarding security, responses, and so on. We do something similar in routes/directory.js.

Now, when we spin up our server, we can visit http://localhost:3000/api-docs to see this:

Swagger UI on localhost

From right within the Swagger UI, we can send requests to our API. For example, we can use the JWT we generated earlier and send an authenticated request to /profile.

Swagger authenticate

Profile request

Import OpenAPI Specification into Postman

The Swagger UI is nice, but we can also use Postman for better programmatic usage and developer experience when it comes to authentication.

In Postman, we click the Import button.

Import into Postman

We can import our OpenAPI specification using a URL. Our Swagger UI shows that the specification is available at http://localhost:3000/api-docs/json. We provide this URL to Postman, choosing to import the API as a Postman Collection.

Import as Postman collection

Now, we have a new collection in Postman with requests set up to hit our API:

Postman collection

When we click on the profile’s GET request, and then click on the Authorization tab, we see that Postman expects two variables: baseUrl and bearerToken.

Postman bearerToken variable

Let’s set the values for those. Go to the options for our Postman Collection, navigating to the Variables tab. There, we set baseUrl to http://localhost:3000. Then, we add a new variable called bearerToken, and we use the value of the valid JWT generated earlier.

Postman variables

Click Save in the upper-right corner. Then, we go back to our /profile request and click Send.

Request response

Going from our OpenAPI specification to Postman is so quick and easy!

Deploy the API to Heroku

As an API developer, you want to spend your development time focused on building and coding. Ideally, deploying your APIs is fast and painless. With Heroku, it is!

Assuming you installed the Heroku CLI, here’s how to deploy your API.

Step 1: Log in

heroku login

Step 2: Create a new app

heroku create my-fastify-api
Creating ⬢ my-fastify-api... done
https://my-fastify-api-58737de5faf0.herokuapp.com/ | https://git.heroku.com/my-fastify-api.git

Step 3: Add the Heroku Postgres add-on

heroku addons:create heroku-postgresql
Creating heroku-postgresql on ⬢ my-fastify-api... ~$0.007/hour (max $5/month)
Database has been created and is available

Step 4: Load the database schema and seed data

heroku pg:psql < data/create_schema.sql
CREATE TABLE

heroku pg:psql < data/create_records.sql
INSERT 0 50

Step 5: Add your RSA public key as a config variable

heroku config:set \
 RSA_PUBLIC_KEY_BASE_64=`cat utils/keys/public_key.example.rsa | base64`
Setting RSA_PUBLIC_KEY_BASE_64 and restarting ⬢ my-fastify-api... done

Step 6: Create a Git remote to point to Heroku

heroku git:remote -a my-fastify-api
set git remote heroku to https://git.heroku.com/my-fastify-api.git

Step 7: Push your repository branch to Heroku

git push heroku main
…
remote: -----> Creating runtime environment
…
remote: -----> Installing dependencies
…
remote: -----> Build succeeded!
…
remote: -----> Launching...
remote:        Released v6
remote:        https://my-fastify-api-58737de5faf0.herokuapp.com/ deployed to Heroku
…

That’s it! Just a few commands in the Heroku CLI, and our API is deployed, configured, and running. Let’s do some checks to make sure.

At the command line, with curl:

curl https://my-fastify-api-58737de5faf0.herokuapp.com/directory
[{"username":"adelia.casper"},{"username":"aisha.upton"},{"username":"alfred.lindgren"},{"username":"alysha.mclaughlin"},{"username":"angie.keebler"},{"username":"antonia.gutmann"},{"username":"baron.hessel"},{"username":"bernadine.powlowski"},{"username":"carlee.abbott"},{"username":"charley.glover"},{"username":"cora.bednar"},{"username":"darryl.reynolds"},{"username":"dee.gorczany"},{"username":"dennis.koss"},{"username":"deshaun.wiza"},{"username":"devante.lakin"},{"username":"edythe.thompson"},{"username":"eldon.bahringer"},{"username":"elenor.trantow"},{"username":"elijah.hane"},{"username":"erin.haley"},{"username":"estefania.will"},{"username":"haven.rippin"},{"username":"houston.rowe"},{"username":"imani.okon"},{"username":"irma.durgan"},{"username":"jaiden.vandervort"},{"username":"jamar.maggio"},{"username":"jamir.walsh"},{"username":"jedediah.mraz"},{"username":"jett.beier"},{"username":"johnathon.hessel"},{"username":"jovan.turner"},{"username":"kade.hilpert"},{"username":"king.berge"},{"username":"laurie.marquardt"},{"username":"madge.hettinger"},{"username":"magali.terry"},{"username":"magdalena.farrell"},{"username":"marty.wunsch"},{"username":"mellie.donnelly"},{"username":"muriel.walker"},{"username":"noelia.jenkins"},{"username":"nolan.dubuque"},{"username":"otis.grady"},{"username":"rene.bins"},{"username":"rhoda.bashirian"},{"username":"rose.boehm"},{"username":"tatyana.wolf"},{"username":"zion.reichel"}]%

In Postman, with an updated baseUrl to point to our Heroku app URL (while keeping the valid bearerToken):

Postman baseUrl on Heroku

And finally, in our browser, checking out the API docs:

Swagger UI on Heroku

Conclusion

When building a Node.js API, using the Fastify framework helps you get up and running quickly. You have access to a rich ecosystem of existing plugins, and building your own plugins is simple and straightforward too. Here’s a quick rundown of everything we did in this walkthrough:

  • Used Fastify to build an API server
  • Built plugins for database querying and JWT authentication
  • Built two routes (one public, one protected) for our API
  • Integrated OpenAPI-related plugins to get an OpenAPI specification and a Swagger UI
  • Showed how to import our OpenAPI specification into Postman
  • Deployed our API to Heroku with just a few commands

With technologies like Fastify, JSON web tokens, and OpenAPI, you can quickly build APIs that are powerful, secure, and easy to consume. Then, when it’s time to deploy and run your code, going with Heroku gets you up and running‌ — ‌_within minutes‌ — ‌_at a low cost. When you’re ready to get started, sign up for a Heroku account and begin building today!

Originally published: April 29, 2024

Browse the archives for engineering or all blogs Subscribe to the RSS feed for engineering or all blogs.