Backend with Fastify - Part 4 (Seeding Database with Knex)

Continuing from part 3, we will explore how to seed data using Knex.

To follow along, you can use the part-3 branch from this repo. The full code for this post is in the part-4 branch.

Power of seeding

Seed files allow us to populate the database with default data, ensuring a consistent starting point for development, testing, or demo environments. They play a crucial role in maintaining a clean and controlled database state.

Setting up seed scripts

Similar to migrations, Knex provides commands for seeding. Let's begin by adding two scripts to our package.json:

{
    scripts: {
        // previous
            "seed:make": "knex seed:make",
            "seed:run": "knex seed:run"
    }
}

These scripts, just like migration commands, enable us to create seeding files and run them. To create a new seed file, execute:

npm run seed:make insert_users

This will generate a file at ./seeds/development/insert_users.ts. Exclude seed files from TypeScript compilation by modifying tsconfig.json:

{
    "exclude": [//previous,"seeds/**/*.ts"]
}

Also, add seeds to .eslintignore.

Hashing Passwords

Before diving into seeding, let's set up a utility function for password hashing. Create a new file generate_hash.ts inside src/utils:

import * as crypto from 'crypto'
import * as util from 'util'
const pbkdf2 = util.promisify(crypto.pbkdf2)
export const generateHash = async (password: crypto.BinaryLike, salt?: crypto.BinaryLike) => {
  if (!salt) {
    salt = crypto.randomBytes(16).toString('hex')
  }
  const hash = (await pbkdf2(password, salt, 1000, 64, 'sha256')).toString('hex')
  return { salt, hash }
}

Now, we're ready to hash passwords securely.

Seeding Default Users

In our seeding file (insert_users.ts), we'll insert two default users:

import { Knex } from 'knex'
import { generateHash } from '../../src/utils/generate_hash'

export async function seed(knex: Knex): Promise<void> {
  // Check if users already exist
  const user1Exists = await knex('users')
    .where('email', 'example1@favmov.com')
    .first()
  const user2Exists = await knex('users')
    .where('email', 'example2@favmov.com')
    .first()

  // If both users do not exist, insert them
  if (!user1Exists) {
    const { salt, hash } = await generateHash('password1')
    await knex('users').insert({
      email: 'example1@favmov.com',
      password: hash,
      salt: salt,
    })
  }

  if (!user2Exists) {
    const { salt, hash } = await generateHash('password2')
    await knex('users').insert({
      email: 'example2@favmov.com',
      password: hash,
      salt: salt,
    })
  }
}

According to Knex's official docs:

"Seed files are executed in alphabetical order. Unlike migrations, every seed file will be executed when you run the command. You should design your seed files to reset tables as needed before inserting data."

So, I chose to check for the user and insert them if not already present.

Now, to run the seed:

npm run seed:run

With this, we have default users. In the next part, we will return to Fastify and explore how to perform authentication and create authenticated routes.