How to build a Pokedex with Platformatic

Recently, Matteo Collina, one of Fastify's creators and much more, launched Platformatic: a fast backend development platform. Since it is built on top of Fastify, it claims to be a life changer, and I want to try it and write down my thoughts!

So, what are we going to build? A Pokedex!

A Pokedex is a fictional device from the Pokemon franchise that is capable of showing information regarding the various species of the Pokémon universe.

Of course, I don't want to write a boring article, let's make it a bit more fun!
Here are the requirements:

  • The Pokemon database already exists. It will be our "legacy" system

  • I want to build a React frontend. It will test Platformatic's "extensibility". Note that Platformatic is a "Backend" framework, but fastify can handle frontend too!

  • The application must be read-only. I don't want to expose any API with write privileges.

The final application architecture will look like:

Now that we have the requirements let's start!

Creating the Platformatic project

We can start by creating the Platformatic project. I followed the Platformatic documentation that is well-written and helpful.

So, I'm going to recap what I did briefly.

Installation

The installation requires Node.js >= v18.8.0, and then we can run one single command:

npm create platformatic@latest

Then, the installer will ask you some questions.
Here is my complete output:

Need to install the following packages:
  create-platformatic@0.12.1
Ok to proceed? (y) y
 Hello, Manuel Spigolon welcome to Platformatic 0.12.1!
 Let's start by creating a new project.
? Which kind of project do you want to create? DB
? Where would you like to create your project? .
? Do you want to create default migrations? yes
? Do you want to create a plugin? yes
? Do you want to use TypeScript? no
[10:25:02] INFO: Configuration file platformatic.db.json successfully created.
[10:25:02] INFO: Environment file .env successfully created.
[10:25:02] INFO: Migrations folder migrations successfully created.
[10:25:02] INFO: Migration file 001.do.sql successfully created.
[10:25:02] INFO: Migration file 001.undo.sql successfully created.
[10:25:02] INFO: Plugin file created at plugin.js
? Do you want to run npm install? yes
✔ ...done!
? Do you want to apply migrations? no
? Do you want to generate types? no
[10:26:36] INFO: Configuration schema successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud? yes
[10:26:40] INFO: Github action successfully created, please add PLATFORMATIC_API_KEY as repository secret.

All done! Please open the project directory and check the README.

After this setup, we have an empty project ready to be adapted to our needs.

Database Preparation

The migrations/ folder was created during the project generation.
This folder will contain the database schema and the data that Platformatic will use to create the SQLite database.

Here must remove all the files and create:

  • The 001.do.schema.sql file that will contain the database schema.

  • The 002.do.data.sql file that will contain the database data.

These files represent our "legacy" system.

The following image shows the raw Entity-Relation schema for our Pokedex:

We must write down the SQL code into the 001.do.schema.sql file. It will be the first migration file to execute.

Then we need to fill the schema so that we will extract the data from Poke API. I used the following GraphQL query to collect all the Pokemon data:

query gottaCatchThemAll {
  pokemon: pokemon_v2_pokemon {
    id
    name
    height
    weight
    types: pokemon_v2_pokemontypes {
      type: pokemon_v2_type {
        name
        id
      }
    }
    images: pokemon_v2_pokemonsprites {
      sprites
    }
    specy:pokemon_v2_pokemonspecy {
      generation_id
      is_baby
      is_legendary
      is_mythical
      color:pokemon_v2_pokemoncolor {
        name
        id
      }
      evolutions: pokemon_v2_evolutionchain {
        baby_trigger_item_id
        id
        chain: pokemon_v2_pokemonspecies {
          id
          order
        }
      }
    }
  }
}

Then, with a simple magical Node.js script, we can save the INSERT statements into a 002.do.data.sql file.

At this point, we have the database schema and the data.
Now we are ready to wire Platformatic with our custom database.
To do so, I created some additional scripts in the package.json:

{
  "scripts": {
    "start": "platformatic db start",
    "db:migrations": "platformatic db migrations apply",
    "db:types": "platformatic db types"
  }
}

I love this setup because it will execute the installed platformatic CLI and I must not remember complex commands

So, by running the npm run db:migrations command:

  • it will create an SQLite database with our Pokemons!

  • the types/ folder is generated to help us during the development phase!

We are ready to execute npm start to spin up our server!
If all is correctly working you will be able to open a browser at http://localhost:3042/pokemon/6 to see the most powerful Pokemon 🔥!

Adding a User Interface to Platformatic

This website is backend.cafe, so I'm not going to annoy you explaining how I built the Pokedex UI, I'm still improving my frontend skill set. It is worth mentioning that the React.js application and the Platformatic auto-reload are nice and shiny during the implementation phase.

Here is a preview of what I have built so far:

This UI has some challenges for the backend too:

  • Serve the website pages

  • A very long list to show with pagination

  • A search form

  • A <select> input item with a list of the database's data

Let's solve it all!

Serve a static website

To serve static files with Fastify, you need to use @fastify/static plugin.
And I used it with Platformatic because all the Fastify's plugins are compatible!

As a Fastify user, doing it has been effortless. I created a /static-website.js file that does what I need:

const path = require('path')
const fastifyStatic = require('@fastify/static')

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function (app) {
  app.register(fastifyStatic, {
    root: path.join(__dirname, 'pokedex-ui/build'),
    decorateReply: false
  })
}

The code is quite straightforward but has one fabulous addition.
The jsdoc comment before the module.exports statement enables a cool autocompletion feature adding your whole database!

This pattern works for TypeScript users and pure JavaScript developers too!

After implementing our custom code, we must integrate it into Platformatic, so we need to edit the core of our Platformatic application, the platformatic.db.json file.
This configuration file controls everything on our application, such as:

  • The HTTP server

  • The additional plugins and integrations

  • The different environments

  • The authorizations

  • The application metrics and monitoring

  • ..and many other things

In our case, we need to add our static-website.js to the plugins section and turn the dashboard offline:

{
  // ... other Platformatic settings
  "dashboard": false,
  "plugin": [
    {
      "path": "static-website.js"
    }
  ]
}

By default, Platformatic serves a dashboard as the root endpoint /.
It is insightful to explore all the endpoints Platformatic generated for us.
In my case, I wanted to serve the Pokedex UI as the root path, so I had to turn it off. (Note there is a feature request to customize the dashboard endpoint)

How to implement the pagination and the search form

Well... I don't have too much to say here because it works out of the box!
To implement it, I need two queries:

  • One to search a slice of the whole dataset

  • One to count the whole dataset by using the same filters of the search query

Under the hood, Platformatic is using the @platformatic/sql-mapper plugin to generate a set of APIs from a database schema. Here, you can find a complete list of the generated endpoints. This plugin can generate what you need to implement the pagination and the search form without any extra configuration!

The queries are the following:

query searchPokemon($limit: LimitInt, $offset: Int, $name: String, $gen: [Int]) {
  pokemon(limit: $limit, offset: $offset, where: {name: {like: $name}, generation: { in: $gen } }) {
    id
    name
    picture { url }
    isLegendary
  }
}

query countSearchPokemon($name: String, $gen: [Int]) {
  countPokemon(where: {name: {like: $name}, generation: { in: $gen }}) {
    total
  }
}

As you can see, the only difference is that the first query manages the limit and offset parameters that are a standard de facto for every pagination.

Moreover, every generated endpoint has a complete query system to filter the data!

I enjoyed focusing only on my Pokedex UI, without needing to implement or change something in the backend.

How to run a custom query

The Generation select item in the search box should list all the Pokemon's generations. This query is too specific, and our database schema doesn't facilitate how Platformatic generates such a query. So we need to write a custom endpoint!

Since Platformatic generates REST and GraphQL endpoints by default, we need to choose if we want to implement the custom endpoint as REST or GQL or both: it is up to us.

I will go for the GQL one because my UI relies on GraphQL to communicate with the backend.

The operation consists in two steps:

  1. Extend the GQL Schema by declaring the custom Query

  2. Implment the new Query resolver

If you don't know GQL and these steps are not clear, I think that reading these articles will help you to introduce yourself to GraphQL.

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function (app) {
  // 1. Extend the GQL Schema
  app.graphql.extendSchema(`
    extend type Query {
      generations: [Int]
    }
  `)

  // 2. Implement the resolver
  app.graphql.defineResolvers({
    Query: {
      generations: async function (source, args, context, info) {
        const sql = app.platformatic.sql('SELECT DISTINCT generation FROM Pokemon ORDER BY generation ASC')
        const generations = await app.platformatic.db.query(sql)
        return generations.map(g => g.generation)
      }
    }
  })
}

The handler runs a raw SQL query and returns the results - nice and easy. As documented, the app.platformatic.sql decorator returns the @database instance already configured a ready to be used. This module provides a comprehensive set of features to query your database, protecting it from SQL injections.

How to expose read-only endpoints

By default, Platformatic doesn't perform any authorization check, but we can configure it in theplatformatic.db.json file. Adding a simple authorization: {} property will turn authentication on.
This configuration will block everything because this setup is all blocked by default now.

Since we want to provide read access only, we need to list all the entities we want to grant read access.

Here is an example of the output configuration:

{
  // .. other configuration properties
  "authorization": {
    "rules": [
      {
        "role": "anonymous",
        "entity": "pokemon",
        "find": true,
        "save": false,
        "delete": false
      },
      {
        "role": "anonymous",
        "entity": "pokemonElement",
        "find": true,
        "save": false,
        "delete": false
      },
      {
        "role": "anonymous",
        "entity": "picture",
        "find": true,
        "save": false,
        "delete": false
      }
      // .. repeat for every database entity
      // until this new shortcut will be released 🎉
      // https://github.com/platformatic/platformatic/issues/658
    ]
  }
}

With the previous setup, we are granting to any anonymous users the find operation while blocking the save (aka insert and update) and the delete ones. This example is simple with true and false values, but every rule item may contain more complex checks as broadly documented.

Restarting our Platformatic service with the new configuration, will block any DELETE or PATCH calls to our endpoints 🛡️

How to deploy it?

This step is always a pain for me because I need to search for a small and free infrastructure where I can publish my experiments and skill up - possibly without providing my credit card!

If you read the installation process output carefully, there was this option:

Do you want to create the github action to deploy this application to Platformatic Cloud? yes

So, the deployment to the Platformatic beta environment took me these steps:

Nice and shiny! So, by opening a pull request, I get a clear message that link me my live application!
I never try a smoother process than this one! 👏

Summary

After building this small project, I think Platformatic is not just an ORM as it may seem but an enhanced version of Fastify.
It implements a lot of good practices and boring stuff that enable us to spin up a fastify instance with:

  • A solid database interface and upgrade process

  • A good authentication and authorization

  • Application already wired to gather metric and measurements

  • Easy to apply CORS settings

  • ..all this is extendible with custom Fastify plugins, so all you already did can still be used

Of course, we did not cover all these topics in this article but I hope you would like to try them out. The last important thing to mention is that Platformatic has not yet reached the v1 release. It is still under development and adds many new cool features at every release.
I'm curious to know what the v1 version will include!

As always, you can find the source code at https://github.com/Eomm/pokedex.
If you enjoyed this article, comment, share, and follow me on Twitter @ManuEomm!