Going full-stack on Astro with Cloudflare D1 and Drizzle
A step-by-step guide to adding a back-end to your Astro project using Cloudflare D1 and Drizzle ORM
I recently wrote about adding /shared links to my website, and wanted to document how I went about setting it up for anyone who wants to do the same!
Installing Astro #
This is pretty straightforward — running the following:
npm create astro@latest
- Select
Empty
for the template - Opt-in to the strictest TypeScript
- Defaults are fine for everything else
Then you can cd
into your project and run npm run dev
Adding Cloudflare adapter #
In your project, you can now run:
npx astro add cloudflare
Say yes to everytyhing, then commit everything and push it up to Github.
Deploying to Pages #
Head over to the Create Pages Application page in the Cloudflare dashboard and click “Connect to git” to create a Pages application using the Github repo.
Be sure to select the Astro Framework preset!
Install wrangler and log in #
If you haven’t already done this, install wrangler
and log in by running:
npm i -g wrangler
wrangler login
Create D1 databases #
We’re going to create two databases, one for production, and one for preview builds.
To do this, run the following commands:
wrangler d1 create d1-demo-prod-db
wrangler d1 create d1-demo-preview-db
Create wrangler.toml
file
#
We’re going to need a wrangler.toml file with the database_id
from each of the databases
we just created.
# wrangler.toml
node_compat = true
[[d1_databases]]
binding = "DB"
database_name = "d1-demo-prod-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
preview_database_id = "DB"
[env.preview]
name = "preview"
[[env.preview.d1_databases]]
binding = "DB"
database_name = "d1-demo-preview-db"
database_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
Update .gitignore
#
echo .wrangler >> .gitignore
echo .drizzle.env >> .gitignore
Update astro.config.ts
#
We need to add the D1 binding like this:
// astro.config.ts
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
// https://astro.build/config
export default defineConfig({
output: "server",
adapter: cloudflare({
platformProxy: {
enabled: true,
},
}),
});
Install Drizzle dependencies #
Run the following commands:
npm i drizzle-orm
npm i -D better-sqlite3 drizzle-kit cross-env @types/node
Create drizzle.config.ts
#
As of this writing, there’s an open issue for better support of drizzle-studio
with D1 projects, so we’re going to use a workaround
for now. This is what our drizzle.config.ts
file will look like:
// drizzle.config.ts
import type { Config } from "drizzle-kit";
const { LOCAL_DB_PATH, DB_ID, D1_TOKEN, CF_ACCOUNT_ID } = process.env;
// Use better-sqlite driver for local development
export default LOCAL_DB_PATH
? ({
schema: "./src/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: LOCAL_DB_PATH,
},
} satisfies Config)
: ({
schema: "./src/schema.ts",
out: "./migrations",
dialect: "sqlite",
driver: "d1-http",
dbCredentials: {
databaseId: DB_ID!,
token: D1_TOKEN!,
accountId: CF_ACCOUNT_ID!,
},
} satisfies Config);
Create your schema #
Let’s go ahead and create src/schema.ts
// src/schema.ts
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const linkShare = sqliteTable("linkShare", {
id: integer("id").primaryKey({
autoIncrement: true,
}),
url: text("url").notNull(),
title: text("title").notNull(),
remark: text("remark"),
created: integer("created", {
mode: "timestamp_ms",
})
.notNull()
.$defaultFn(() => new Date()),
modified: integer("modified", {
mode: "timestamp_ms",
})
.notNull()
.$defaultFn(() => new Date()),
deleted: integer("deleted", {
mode: "timestamp_ms",
}),
});
Add scripts to package.json
#
Let’s add the following npm scripts to our package.json
’s scripts
.
Be sure to update the DB_ID
values according to your preview and prod ID’s generated
in the previous steps!
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate:local": "wrangler d1 migrations apply d1-demo-prod-db --local",
"db:migrate:prod": "wrangler d1 migrations apply d1-demo-prod-db --remote",
"db:migrate:preview": "wrangler d1 migrations apply --env preview d1-demo-preview-db --remote",
"db:studio:local": "LOCAL_DB_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) drizzle-kit studio",
"db:studio:preview": "source .drizzle.env && DB_ID='yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' drizzle-kit studio",
"db:studio:prod": "source .drizzle.env && DB_ID='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' drizzle-kit studio"
}
}
Generate migrations #
Now we’re going to use Drizzle to generate the first migration to apply!
npm run db:generate
Apply the migration locally #
We will run
npm run db:migrate:local
This will apply the migration to our local database!
Interact with local DB using Drizzle Studio #
Now we can inspect the local database by running
npm run db:studio:local
Try inserting a row into the linkShares
table adding only the url
and title
!
Add DB to locals #
To generate our D1 binding types, we need to run:
npx wrangler types
Then update src/env.d.ts
as follows:
// src/env.d.ts
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
type Runtime = import("@astrojs/cloudflare").Runtime<Env>;
declare namespace App {
interface Locals extends Runtime {}
}
Render the links #
In src/pages/index.astro
, you can now query the D1 database:
---
// src/pages/index.astro
import { desc } from "drizzle-orm";
import { drizzle } from "drizzle-orm/d1";
import { linkShare } from "../schema";
const db = drizzle(Astro.locals.runtime.env.DB);
const links = await db
.select()
.from(linkShare)
.orderBy(desc(linkShare.created));
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>Shared Links</h1>
<ul>
{
links.map((link) => (
<li>
<a href={link.url}>{link.title}</a>
</li>
))
}
</ul>
</body>
</html>
At this point, you should be able to run the local dev server again:
npm run dev
Add bindings in Pages project #
- In the Cloudflare dashboard, go to your Pages projects and open your project.
- Go to Settings > Functions > D1 database bindings
- Under the “Production” tab, click “Add binding”, and name it
DB
. Bind this to thed1-demo-prod-db
database we created earlier. - Under the “Preview” tab, click “Add binding”, and name it
DB
. Bind this to thed1-demo-preview-db
database we created earlier.
Run migrations for preview and prod #
npm run db:migrate:preview
npm run db:migrate:prod
Create .drizzle.env
#
Since prod and preview are going to use the d1-http
adapter, we need to provide these tokens
in the .drizzle.env
file. You can create this token here.
export D1_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaa
export CF_ACCOUNT_ID=bbbbbbbbbbbbbbbbbbbbbbbbbbbb
Add data to preview and prod environments #
Use Drizzle Studio again to populate some data in the preview environment by running:
npm run db:studio:preview
Then do the same for prod by running:
npm run db:studio:prod
Commit and open a PR #
It’s time to test the preview build! Let’s create a branch and push it up.
git checkout -b testing-preview-builds
git add .
git commit -m "D1 and Drizzle setup"
git push -u origin HEAD
The preview build should work! Go ahead and merge it, and prod should work too! 🎉
That’s all! #
You can check out the completed code on Github.