One of the most exciting parts of building this site has been working with Cloudflare’s developer tools. They’re truly cloud-native, designed for performance, and offer immense value to developers looking to build scalable applications.

Why Cloudflare D1?

I’ve had my eye on Cloudflare D1 for a while. NoSQL databases are everywhere, but relational databases still shine when structured data is key. D1 delivers the power of SQL without the usual headaches of latency, replication, or server management.

What Makes D1 Stand Out?

For a full breakdown, check out Cloudflare’s D1 announcement.

Building a Sample Project with D1

My blog primarily uses Markdown as a datastore, so I needed to create an app that would benefit from structured data.

I decided to create a shirt catalog using D1 as the backend. It’s a fun way to experiment, document my designs, and build a more interactive content system.

a lineup of t-shirts

Project Goals

Setting Up the Cloudflare D1

Thanks to Wrangler, setting up D1 is quick and painless.

1️⃣ Create the Database

Run:

wrangler d1 create <db_name>

Your database will now appear in Cloudflare’s Dashboard under Workers & Pages → D1.

2️⃣ Configure Wrangler

Add the reference to wrangler.toml:

[[d1_databases]]
binding = "DB" # customizable
database_name = "[DB_NAME]"
database_id = "[DB_ID]"

3️⃣ Generate TypeScript Types

npm run cf-typegen

Now, TypeScript can reference the database using the binding name.

4️⃣ Populate the Database with SQL

wrangler d1 execute <db_name> --local --schema.sql
DROP TABLE IF EXISTS Shirts;

CREATE TABLE
    IF NOT EXISTS Shirts (
        id INTEGER PRIMARY KEY,
        name TEXT,
        createdDate TEXT
    );

INSERT INTO
    Shirts (
        id,
        name,
        createdDate
    )
VALUES
    (
        1,
        'Rainbow Deciduous Tee',
        "2024-03-02 21:16:35.000"
    );    

Local vs. Remote Databases

Key Concept:

For more details, check out Cloudflare’s discussion on this design choice.

Using D1 with Remix

1️⃣ Fetching Data with a Remix Loader

I’m using worker-qb to simplify queries.

import type { LoaderFunctionArgs } from "@remix-run/cloudflare";
import { D1QB, OrderTypes } from 'workers-qb'

export const loader = async ({ context }: LoaderFunctionArgs) => {
    const { env } = context.cloudflare;
    const qb = new D1QB(env.DB)

    const query = await qb
        .fetchAll<Shirt>({
            tableName: 'shirts',
            orderBy: { 'createdDate': OrderTypes.DESC },
        })
        .execute();

    const shirts = query.results;

    return json(shirts);
};

Key Concept: Use LoaderFunctionArgs to ensure type inference when passing data to other functions.

2️⃣ Using the Data in the Page Component

export default function ShirtsIndex() {
    const shirts = useLoaderData<typeof loader>();

    return (
        <>
            <h1>Shirts</h1>
            {shirts.map((shirt) =>
                <div key={shirt.id}>
                    {shirt.name}
                </div >
            )}
        </>
    )
}

Final Thoughts & Next Steps

Once I got past the two key concepts—understanding how D1 separates local and production databases, and optimizing data queries in Remix—everything else fell into place.

Next, I’ll be:

a shirt detail page

Epilogue: Features I Need to Build

Writing this post made me realize I’m missing key blog features. Here’s what I've added to my to-do list:

Plenty more to build—stay tuned! 🚀