Serverless NextJS with Drizzle-ORM for Cloudflare

Published

In today's web development landscape, leveraging the right tools and services is pivotal in creating efficient, scalable, and secure applications. This blog post aims to guide you through the process of setting up a Next.js application, enhanced with Cloudflare Workers for serverless functionality and Drizzle ORM for streamlined database management. Whether you're building a complex application or a simple website, this setup ensures performance, security, and ease of maintenance.

Setting up Cloudflare

1 - init project

We're going to use the cloudflare template for NextJS given from their docs here.

This will setup some basic things for us to start customizing, but not too much that it feels overwhelming for development compared to the standard NextJS inits.

npm create cloudflare@latest my-next-app -- --framework=next

2 - create D1 instance

We want to create a cloudflare d1 instance, this uses a sqlite database that is edge-compatible.

npx wrangler d1 create <db-name>

Then, copy the output into wrangler.toml in the project root. We will also need to configure the DB setting in the Cloudflare page settings to <db-name>

3 - update env.d.ts types

We will need to update the CloudflareEnv type so we can access our D1 Database within the code.

npx wrangler types --env-interface CloudflareEnv env.d.ts

This command should update the env.d.ts file automatically.

Installing Drizzle

4 - install drizzle dependencies

npm i drizzle-orm
npm i -D drizzle-kit

5 - drizzle config

Create a drizzle.config.ts file in root

import { defineConfig } from "drizzle-kit";

// using cloudflare wrangler
export default defineConfig({
    schema: "./src/db/schema.ts",
    dbCredentials: {
        dbName: "chamber-db",
        wranglerConfigPath: "."
    },
    driver: "d1"
})

6 - create schema file

Create schema.ts in src/db directory

// src/db/schema.ts
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const roadmap_tasks = sqliteTable("roadmap_tasks", {
    id: integer("id").primaryKey({ autoIncrement: true }),
    title: text("title"),
    description: text("description"),
});

7 - wrapping d1 with drizzle adapter

Add d1 driver for drizzle. This allows us to use db as the standard drizzle variable. This code is a little hacky in order to get the project to build without errors, as getRequestContext cannot be called outside of a request, but will be called when importing, which is outside of a request function.

// src/db/db.ts
import { drizzle } from "drizzle-orm/d1";
import * as schema from "@/db/schema";
import { getRequestContext } from "@cloudflare/next-on-pages";

function getDB() {
    if (process.env.NODE_ENV === "development") {
        const { env } = getRequestContext();
        return drizzle(env.DB, { schema });
    }
    // Production
    return drizzle(process.env.DB, { schema });
}

export const db = getDB();

In order to get proper types, we need to add this to env.d.ts

namespace NodeJS {
    interface ProcessEnv {
        DB: D1Database
    }
}

Testing & Deployment

8 - add a test route

Here we're overwriting the app/api/hello/route.ts. This will fetch whatever we put in roadmap_tasks. You can add these to your local DB manually, or using the cloudflare d1 interface for production.

import { getRequestContext } from '@cloudflare/next-on-pages'
import { db } from '@/db/db'
import { roadmap_tasks } from '@/db/schema'

export const runtime = 'edge'

export async function GET() {
    const result = await db.select().from(roadmap_tasks);

    return new Response(JSON.stringify(result));
}

9 - setting up migration & development scripts

Modify your package.json with these scripts

"scripts": {
    "dev": "npx wrangler pages dev --local --d1=DB -- next dev",
    "schema": "npx drizzle-kit generate:sqlite --schema=./src/db/schema.ts --out=./src/db/migrations",
    "migrate": "npx wrangler d1 migrations apply chamber-db --local",
    "migrate-prod": "npx wrangler d1 migrations apply chamber-db --remote"
    ...
}

and then in wrangler.toml under [[d1_databases]]

migrations_dir = "src/db/migrations"

10 - deploying

Run these commands

npm run schema
npm run migrate-prod
npm run deploy