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.
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.
For a full breakdown, check out Cloudflare’s D1 announcement.
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.
Thanks to Wrangler, setting up D1 is quick and painless.
Run:
wrangler d1 create <db_name>
Your database will now appear in Cloudflare’s Dashboard under Workers & Pages → D1.
Add the reference to wrangler.toml
:
[[d1_databases]]
binding = "DB" # customizable
database_name = "[DB_NAME]"
database_id = "[DB_ID]"
npm run cf-typegen
Now, TypeScript can reference the database using the binding name.
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"
);
Key Concept:
For more details, check out Cloudflare’s discussion on this design choice.
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.
export default function ShirtsIndex() {
const shirts = useLoaderData<typeof loader>();
return (
<>
<h1>Shirts</h1>
{shirts.map((shirt) =>
<div key={shirt.id}>
{shirt.name}
</div >
)}
</>
)
}
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:
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! 🚀