DEV Community

Cover image for Fastify API with Postgres and Drizzle ORM
Vladimir Vovk
Vladimir Vovk

Posted on β€’ Edited on

2

Fastify API with Postgres and Drizzle ORM

Before we start, we will need Node.js installed.

General Setup

Let's create a new directory for our API first:

mkdir fastify-api
cd fastify-api
Enter fullscreen mode Exit fullscreen mode

Now, we need to initialize a new project:

npm init -y
Enter fullscreen mode Exit fullscreen mode

And install dependencies:

npm install drizzle-orm pg pino pino-pretty fastify @fastify/env dotenv
Enter fullscreen mode Exit fullscreen mode

Also, we will need some packages for development:

npm install --dev typescript tsx drizzle-kit @types/pg tsc-alias
Enter fullscreen mode Exit fullscreen mode

TypeScript

As we want to use TypeScript, we must create a tsconfig.json configuration file. Let's create a default configuration with:

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

It's good to have all source files in one place. So, let's create a src folder for all our code:

mkdir src
Enter fullscreen mode Exit fullscreen mode

Now that we have the src folder, it will be nice to use "absolute" path imports. E.g. import { funcA } from 'src/modules/moduleA'. Notice that the path starts with src instead of ../../.. dots when we use "relative" paths. Let's add the baseUrl parameter to our tsconfig.json:

{
  "compilerOptions": {
    ...
    "baseUrl": "./"
}
Enter fullscreen mode Exit fullscreen mode

Awesome! We have just a couple of options left to be able to compile our TypeScript files:

{
  "compilerOptions": {
    ...
    "rootDir": "./src",
    "outDir": "./dist"
  },
  ...
  "include": ["src/**/*.ts"]
}
Enter fullscreen mode Exit fullscreen mode

Let's add build and start commands to our package.json:

{
  ...
  "scripts": {
    ...
    "build": "tsc -p tsconfig.json && tsc-alias",
    "start": "node ./dist/main.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

❗ Note the tsc-alias command. It will replace absolute paths with relative paths after the typescript compilation.

Huh! It looks like we are ready to write some code. Let's create the main.ts file inside the src directory:

console.log('Hello, world!')
Enter fullscreen mode Exit fullscreen mode

To be able to run this file, we need to add another command to the package.json:

{
  ...
  "scripts": {
    ...
    "dev": "tsx watch src/main.ts" 
  }
}
Enter fullscreen mode Exit fullscreen mode

Hurray! Now, we can run our code with the npm run dev command.

Fastify Server

Let's create a Fastify server inside the src/server.ts file:

import Fastify from 'fastify'

export const createServer = async () => {
  const fastify = Fastify({
    logger: true,
  })

  fastify.get('/ping', (request, reply) => {
    reply.send({ message: 'pong' })
  })

  return fastify
}
Enter fullscreen mode Exit fullscreen mode

Now, we need to update the src/main.ts to run the server:

import { createServer } from 'src/server'

const main = async () => {
  const fastify = await createServer()
  const port = 3000

  try {
    fastify.listen({ port }, () => {
      fastify.log.info(`Listening on ${port}...`)
    })
  } catch (error) {
    fastify.log.error('fastify.listen:', error)
    process.exit(1)
  }
}

main()
Enter fullscreen mode Exit fullscreen mode

To check the server, open the browser and navigate to the localhost:3000/ping. We should see the { message: 'pong' } response. πŸŽ‰

Environment Variables

We set the server port to 3000, which could be a problem if we deploy our service to a Cloud Application Platform (port will be assigned by the platform in this case). Let's use environment variables to get the port number and other parameters.

We will set environment variables with the .env file. Let's create the .env file inside our project directory:

DATABASE_URL='Our database URL. We will set it later'
PINO_LOG_LEVEL=debug
NODE_ENV=development
Enter fullscreen mode Exit fullscreen mode

To be able to load this file, Fastify has a nice @fastify/env plugin. This plugin allows you to set the schema for environment variables and will check that all environment variables are set correctly. But it's only available with Fastify or Request instance, so we will use the dotenv package in "other" cases.

Let's add the @fastify/env plugin configuration to the src/server.ts:

...
import env from '@fastify/env'

const schema = {
  type: 'object',
  required: ['PORT', 'DATABASE_URL'],
  properties: {
    PORT: {
      type: 'string',
      default: 3000,
    },
    DATABASE_URL: {
      type: 'string',
    },
    PINO_LOG_LEVEL: {
      type: 'string',
      default: 'error',
    },
    NODE_ENV: {
      type: 'string',
      default: 'production',
    },
  },
}

const options = {
  schema: schema,
  dotenv: true,
}

declare module 'fastify' {
  interface FastifyInstance {
    config: {
      PORT: string
      DATABASE_URL: string
      PINO_LOG_LEVEL: string
      NODE_ENV: string
    }
  }
}

export const createServer = async () => {
  const fastify = Fastify({
    logger: true,
  })

  /* Register plugins */
  await fastify.register(env, options).after()

  ...
}
Enter fullscreen mode Exit fullscreen mode

And get the port number inside main.ts:

...

const main = async () => {
  const fastify = await createServer()
  const port = Number(fastify.config.PORT)

...
Enter fullscreen mode Exit fullscreen mode

Pino Logger

We did enable logger before inside the createServer function:

  ...
  const fastify = Fastify({
    logger: true,
  })
  ...
Enter fullscreen mode Exit fullscreen mode

And we can use it with the fastify.log or request.log functions. But if we want to use it "outside" of Fastify or Request instances, we need to create a separate logger instance and export it.

Let's create the src/utils/logger.ts file:

import pino, { Level } from 'pino'

export { Level }

type CreateLoggerArgs = {
  level: Level
  isDev: boolean
}

export const createLogger = ({ level, isDev }: CreateLoggerArgs) =>
  pino({
    level,
    redact: ['req.headers.authorization'],
    formatters: {
      level: (label) => {
        return { level: label.toUpperCase() }
      },
    },
    ...(isDev && { transport: { target: 'pino-pretty' } }),
  })
Enter fullscreen mode Exit fullscreen mode

Now we can use it in createServer.ts:

import dotenv from 'dotenv'
import { createLogger, Level } from 'src/utils/logger'

...

dotenv.config()

const level = process.env.PINO_LOG_LEVEL as Level
const isDev = process.env.NODE_ENV === 'development'
const logger = createLogger({ level, isDev })

export { logger }

export const createServer = async () => {
  const fastify = Fastify({
    loggerInstance: logger,
  })

...
Enter fullscreen mode Exit fullscreen mode

We are using the dotenv package here to load environment variables because we can't access the Fastify instance. Then, create the logger instance, export it, and assign it to Fastify.

Drizzle ORM

Let's create src/db/index.ts:

import { Pool } from 'pg'
import { drizzle } from 'drizzle-orm/node-postgres'
import dotenv from 'dotenv'

dotenv.config()

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: true,
})

export const db = drizzle(pool)
Enter fullscreen mode Exit fullscreen mode

Now, we need a schema. Create the src/db/schema.ts:

import {
  pgTable,
  timestamp,
  uuid,
  varchar,
  text,
} from 'drizzle-orm/pg-core'

const timestamps = {
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at')
    .defaultNow()
    .$onUpdate(() => new Date()),
}

export const posts = pgTable('posts', {
  id: uuid('id').primaryKey().defaultRandom(),
  title: varchar('title', { length: 256 }).notNull(),
  content: text('text').notNull(),
  ...timestamps,
})
Enter fullscreen mode Exit fullscreen mode

Before we can create a migration for our schema, we need to create a drizzle.config.ts configuration inside the project root:

import dotenv from 'dotenv'
import { defineConfig } from 'drizzle-kit'

dotenv.config()

export default defineConfig({
  out: './migrations',
  schema: './src/db/schema.ts',
  breakpoints: false,
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL as string,
  },
})
Enter fullscreen mode Exit fullscreen mode

And add new scripts to package.json:

{
  ...
  "scripts": {
    ...
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate"
  },
...
}
Enter fullscreen mode Exit fullscreen mode

Let's create our first migration with:

npm run db:generate
Enter fullscreen mode Exit fullscreen mode

We will need a real database to apply migrations and store our data. You can install Postgres on your machine or use a Cloud Platform like Neon, Render, etc.

❗ Please set the DATABASE_URL parameter inside the .env configuration file.

Let's apply migration to the database:

npm run db:migrate
Enter fullscreen mode Exit fullscreen mode

Now that the posts table is created, we can start to query our data.

Posts Route

We will start from the database query. Let's create the src/modules/posts/db.ts file:

import { desc } from 'drizzle-orm'
import { db } from 'src/db'
import { posts } from 'src/db/schema'

export const getPosts = async () => {
  const result = await db
    .select()
    .from(posts).    
    .orderBy(desc(posts.createdAt))
    .limit(10)

  return result
}
Enter fullscreen mode Exit fullscreen mode

❗ We are selecting the last 10 posts. It's better to implement pagination here instead.

Now we can create the /posts endpoint handler inside the src/modules/posts/handler.ts:

import { FastifyReply, FastifyRequest } from 'fastify'
import { getPosts } from './db'

export const getPostsHandler = async (
  request: FastifyRequest,
  reply: FastifyReply,
) => {
  const data = await getPosts()

  return { data }
}
Enter fullscreen mode Exit fullscreen mode

Let's add the getPostsHandler to the src/modules/posts/router.ts:

import { FastifyInstance } from 'fastify'
import { getPostsHandler } from './handler'

export const postsRouter = (fastify: FastifyInstance) => {
  fastify.get('/', getPostsHandler)
}
Enter fullscreen mode Exit fullscreen mode

Now, we can add the "posts router" to our server. Let's update the server.ts:

...

  fastify.get('/ping', (request, reply) => {
    reply.send({ message: 'pong' })
  })

  /* Add the posts router under the `ping` endpoint */
  fastify.register(postsRouter, { prefix: 'api/posts' })

...
Enter fullscreen mode Exit fullscreen mode

We are using the api/posts prefix, so our posts will be available with the localhost:3000/api/posts URL.

Conclusion

We have learned how to create a Fastify API server and query data from a PostgreSQL database with Drizzle ORM.

Please feel free to use this setup as a foundation for your next app, press the πŸ’– button, and happy hacking!

Credits

Photo by Stephen Dawson on Unsplash

Quadratic AI

Quadratic AI – The Spreadsheet with AI, Code, and Connections

  • AI-Powered Insights: Ask questions in plain English and get instant visualizations
  • Multi-Language Support: Seamlessly switch between Python, SQL, and JavaScript in one workspace
  • Zero Setup Required: Connect to databases or drag-and-drop files straight from your browser
  • Live Collaboration: Work together in real-time, no matter where your team is located
  • Beyond Formulas: Tackle complex analysis that traditional spreadsheets can't handle

Get started for free.

Watch The Demo πŸ“Šβœ¨

Top comments (0)

Billboard image

Try REST API Generation for MS SQL Server.

DevOps for Private APIs. With DreamFactory API Generation, you get:

  • Auto-generated live APIs mapped from database schema
  • Interactive Swagger API documentation
  • Scripting engine to customize your API
  • Built-in role-based access control

Learn more

πŸ‘‹ Kindness is contagious

Dive into this thoughtful article, cherished within the supportive DEV Community. Coders of every background are encouraged to share and grow our collective expertise.

A genuine "thank you" can brighten someone’s dayβ€”drop your appreciation in the comments below!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found value here? A quick thank you to the author makes a big difference.

Okay