diff --git a/src/content/docs/d1/tutorials/using-read-replication-for-e-com/index.mdx b/src/content/docs/d1/tutorials/using-read-replication-for-e-com/index.mdx new file mode 100644 index 000000000000000..5abc9518fd5af2d --- /dev/null +++ b/src/content/docs/d1/tutorials/using-read-replication-for-e-com/index.mdx @@ -0,0 +1,807 @@ +--- +updated: 2025-03-27 +difficulty: Beginner +content_type: Tutorial +pcx_content_type: tutorial +title: Using D1 Read Replication for your e-commerce website +products: + - D1 +languages: + - JavaScript + - TypeScript + - SQL +--- + +import { + Render, + Steps, + PackageManagers, + WranglerConfig, + Details, +} from "~/components"; + +[D1 Read Replication](/d1/features/read-replication/) is a feature that allows you to replicate your D1 database to multiple regions. This is useful for your e-commerce website, as it reduces read latencies and improves availability. In this tutorial, you will learn how to use D1 read replication for your e-commerce website. + +While this tutorial uses a fictional e-commerce website, the principles can be applied to any use-case that requires high availability and low read latencies, such as a news website, a social media platform, or a marketing website. + +## Quick start + +If you want to skip the steps and get started quickly, click on the below button: + +[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/harshil1712/e-com-d1-hono) + +This will create a repository in your GitHub account and deploy the application to Cloudflare Workers. It will also create and bind a D1 database, create the required tables, and add some sample data. You can then visit the deployed application. + +After deploying to Cloudflare through the button, this tutorial requires you to enable read replication for your newly created D1 database. + +To enable D1 Read Replication for your database, run the following command: + +```sh +curl -X PUT "https://api.cloudflare.com/client/v4/accounts//d1/database/" -H "Authorization: Bearer $TOKEN" -H "Content-Type:application/json" --data '{ "read_replication": { "mode": "auto" } }' +``` + +## Prerequisites + + + +## Step 1: Create a Workers project + +Create a new Workers project by running the following command: + + + + + +For creating the API routes, you will use [Hono](https://hono.dev/). You need to install Hono by running the following command: + +```sh +npm install hono +``` + +## Step 2: Update the frontend + +The above step creates a new Workers project with a default frontend and installs Hono. You will update the frontend to list the products. You will also add a new page to the frontend to display a single product. + +Update the `public/index.html` file to list the products. Use the below code as a reference. + +
+```html + + + + + + E-commerce Store + + + +
+

E-commerce Store

+ +
+ +
+ +
+ +
+

© 2025 E-commerce Store. All rights reserved.

+
+ + + + +``` +
+ +Create a new `public/product-details.html` file to display a single product. + +
+```html + + + + + + Product Details - E-commerce Store + + + +
+ E-commerce Store + +
+ +
+ ← Back to products +

Product Name

+

Product description goes here.

+ +
+

$0.00

+

0 in stock

+
+ + +
+ +
Added to cart!
+ +
+

© 2025 E-commerce Store. All rights reserved.

+
+ + + + + +``` +
+ +You now have a frontend that lists products and displays a single product. However, the frontend is not yet connected to the D1 database. If you start the development server now, you will see no products. In the next steps, you will create a D1 database and create APIs to fetch products and display them on the frontend. + +## Step 3: Create a D1 database and enable read replication + +Create a new D1 database by running the following command: + +```sh +npx wrangler d1 create fast-commerce +``` + +Add the D1 bindings returned in the terminal to the `wrangler` file: + + +```toml +[[d1_databases]] +binding = "DB" +database_name = "fast-commerce" +database_id = "YOUR_DATABASE_ID" +``` + + +Run the following command to update the `Env` interface in the `worker-congifuration.d.ts` file. + +```sh +npm run cf-typegen +``` + +Next, enable read replication for the D1 database by running the following command. You will need to replace `` with your Cloudflare account ID, `` with the ID of the D1 database, and `$TOKEN` with your Cloudflare API token. You can learn more about it in the [Read replication documentation](/d1/features/read-replication/#enable-read-replication). + +:::note +Read replication is only used when the application has been [deployed](/d1/tutorials/using-read-replication-for-e-com/#step-8-deploy-the-application). D1 does not create read replicas when you develop locally. To test it locally, you can start the development server with the `--remote` flag. +::: + +```sh +curl -X PUT "https://api.cloudflare.com/client/v4/accounts//d1/database/" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type:application/json" --data '{ "read_replication": { "mode": "auto" } }' +``` + +## Step 4: Create the API routes + + + +Update the `src/index.ts` file to import the Hono library and create the API routes. + +```ts +import { Hono } from 'hono'; +// Set db session bookmark in the cookie +import { getCookie, setCookie } from 'hono/cookie'; + +const app = new Hono<{ Bindings: Env }>(); + +// Get all products +app.get('/api/products', async (c) => { + return c.json({ message: 'get list of products' }); +}); + +// Get a single product +app.get('/api/products/:id', async (c) => { + return c.json({ message: 'get a single product' }); +}); + +// Upsert a product +app.post('/api/product', async (c) => { + return c.json({ message: 'create or update a product' }); +}); + +export default app; +``` + +The above code creates three API routes: + +- `GET /api/products`: Returns a list of products. +- `GET /api/products/:id`: Returns a single product. +- `POST /api/product`: Creates or updates a product. + +However, the API routes are not connected to the D1 database yet. In the next steps, you will create a products table in the D1 database, and update the API routes to connect to the D1 database. + +## Step 5: Create the D1 database schema + +Create a products table in the D1 database by running the following command: + +```sh +npx wrangler d1 execute fast-commerce --command "CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, inventory INTEGER NOT NULL DEFAULT 0, category TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)" +``` + +Next, create an index on the products table by running the following command: + +```sh +npx wrangler d1 execute fast-commerce --command "CREATE INDEX IF NOT EXISTS idx_products_id ON products (id)" +``` + +For development purposes, you can also execute the insert statements on the local D1 database by running the following command: + +```sh +npx wrangler d1 execute fast-commerce --command "INSERT INTO products (id, name, description, price, inventory, category) VALUES (1, 'Fast Ergonomic Chair', 'A comfortable chair for your home or office', 100.00, 10, 'Furniture'), (2, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing'), (3, 'Fast Wooden Desk', 'A wooden desk for your home or office', 150.00, 5, 'Furniture'), (4, 'Fast Leather Sofa', 'A leather sofa for your home or office', 300.00, 3, 'Furniture'), (5, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing')" +``` + + + +## Step 6: Update the API routes + +Update the API routes to connect to the D1 database. + + +### 1. POST /api/product + +```ts +app.post('/api/product', async (c) => { + const product = await c.req.json(); + + if (!product) { + return c.json({ message: 'No data passed' }, 400); + } + + const db = c.env.DB; + const session = db.withSession('first-primary'); + + const { id } = product; + + try { + // Check if the product exists + const { results } = await session.prepare('SELECT * FROM products where id = ?').bind(id).run(); + if (results.length === 0) { + const fields = [...Object.keys(product)]; + const values = [...Object.values(product)]; + // Insert the product + await session + .prepare(`INSERT INTO products (${fields.join(', ')}) VALUES (${fields.map(() => '?').join(', ')})`) + .bind(...values) + .run(); + const latestBookmark = session.getBookmark(); + latestBookmark && + setCookie(c, 'product_bookmark', latestBookmark, { + maxAge: 60 * 60, // 1 hour + }); + return c.json({ message: 'Product inserted' }); + } + + // Update the product + const updates = Object.entries(product) + .filter(([_, value]) => value !== undefined) + .map(([key, _]) => `${key} = ?`) + .join(', '); + + if (!updates) { + throw new Error('No valid fields to update'); + } + + const values = Object.entries(product) + .filter(([_, value]) => value !== undefined) + .map(([_, value]) => value); + + await session + .prepare(`UPDATE products SET ${updates} WHERE id = ?`) + .bind(...[...values, id]) + .run(); + const latestBookmark = session.getBookmark(); + latestBookmark && + setCookie(c, 'product_bookmark', latestBookmark, { + maxAge: 60 * 60, // 1 hour + }); + return c.json({ message: 'Product updated' }); + } catch (e) { + console.error(e); + return c.json({ message: 'Error upserting product' }, 500); + } +}); + +``` + +In the above code: + +- You get the product data from the request body. +- You then check if the product exists in the database. + - If it does, you update the product. + - If it doesn't, you insert the product. +- You then set the bookmark in the cookie. +- Finally, you return the response. + +Since you are writing the data to the database, you use the `first-primary` constraint. Even if you use the `first-unconstrained` constraint or pass a bookmark, the write request will always be routed to the primary database. + +The bookmark set in the cookie can be used to get the data from the database for a new session from that instance. + +If you are using an external platform to manage your products, you can connect this API to the external platform, such that, when a product is created or updated in the external platform, the D1 database automatically updates the product details. + +### 2. GET /api/products + +```ts +app.get('/api/products', async (c) => { + const db = c.env.DB; + + // Get bookmark from the cookie + const bookmark = getCookie(c, 'product_bookmark') || 'first-unconstrained'; + + const session = db.withSession(bookmark); + + try { + const { results } = await session.prepare('SELECT * FROM products').all(); + + const latestBookmark = session.getBookmark(); + + // Set the bookmark in the cookie + latestBookmark && + setCookie(c, 'product_bookmark', latestBookmark, { + maxAge: 60 * 60, // 1 hour + }); + + return c.json(results); + } catch (e) { + console.error(e); + return c.json([]); + } +}); + +``` + +In the above code: + +- You get the database session bookmark from the cookie. + - If the bookmark is not set, you use the `first-unconstrained` constraint. +- You then create a database session with the bookmark. +- You fetch all the products from the database and get the latest bookmark. +- You then set this bookmark in the cookie. +- Finally, you return the results. + +### 3. GET /api/products/:id + +```ts +app.get('/api/products/:id', async (c) => { + const id = c.req.param('id'); + + if (!id) { + return c.json({ message: 'Invalid id' }, 400); + } + + const db = c.env.DB; + + // Get bookmark from the cookie + const bookmark = getCookie(c, 'product_bookmark') || 'first-unconstrained'; + + const session = db.withSession(bookmark); + + try { + const { results } = await session.prepare('SELECT * FROM products where id = ?').bind(id).run(); + + const latestBookmark = session.getBookmark(); + + // Set the bookmark in the cookie + latestBookmark && + setCookie(c, 'product_bookmark', latestBookmark, { + maxAge: 60 * 60, // 1 hour + }); + + console.log(results); + + return c.json(results); + } catch (e) { + console.error(e); + return c.json([]); + } +}); + +``` + +In the above code: +- You get the product ID from the request parameters. +- You then create a database session with the bookmark. +- You fetch the product from the database and get the latest bookmark. +- You then set this bookmark in the cookie. +- Finally, you return the results. + + +## Step 7: Test the application + +You have now updated the API routes to connect to the D1 database. You can now test the application by starting the development server and navigating to the frontend. + +```sh +npm run dev +``` + +Navigate to `http://localhost:8787. You should see the products listed. Click on a product to view the product details. + +To insert a new product, use the following command: + +```sh +curl -X POST http://localhost:8787/api/product \ + -H "Content-Type: application/json" \ + -d '{"id": 6, "name": "Fast Computer", "description": "A computer for your home or office", "price": 1000.00, "inventory": 10, "category": "Electronics"}' +``` + +Navigate to `http://localhost:8787/product-details?id=6`. You should see the new product. + +Update the product using the following command, and navigate to `http://localhost:8787/product-details?id=6` again. You will see the updated product. + +```sh +curl -X POST http://localhost:8787/api/product \ + -H "Content-Type: application/json" \ + -d '{"id": 6, "name": "Fast Computer", "description": "A computer for your home or office", "price": 1050.00, "inventory": 10, "category": "Electronics"}' +``` + +:::note + +Since you have been developing the application locally, the data is not replicated to other regions. To see the data in other regions, you need to deploy the application to Cloudflare. + +::: + +## Step 8: Deploy the application + +Since the database you used in the previous steps is local, you need to create the products table in the remote database. Execute the following D1 commands to create the products table in the remote database. + +```sh +npx wrangler d1 execute fast-commerce --remote --command "CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, inventory INTEGER NOT NULL DEFAULT 0, category TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)" +``` + +Optionally, you can insert the products into the remote database by running the following command: + +```sh +npx wrangler d1 execute fast-commerce --remote --command "INSERT INTO products (id, name, description, price, inventory, category) VALUES (1, 'Fast Ergonomic Chair', 'A comfortable chair for your home or office', 100.00, 10, 'Furniture'), (2, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing'), (3, 'Fast Wooden Desk', 'A wooden desk for your home or office', 150.00, 5, 'Furniture'), (4, 'Fast Leather Sofa', 'A leather sofa for your home or office', 300.00, 3, 'Furniture'), (5, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing')" +``` + +Now, you can deploy the application with the following command: + +```sh +npm run deploy +``` + +This will deploy the application to Workers and the D1 database will be replicated to the remote regions. If a user requests the application from any region, the request will be redirected to the nearest region where the database is replicated. + +## Conclusion + +In this tutorial, you learned how to use D1 Read Replication for your e-commerce website. You created a D1 database and enabled read replication for it. You then created an API to create and update products in the database. You also learned how to use the bookmark to get the latest data from the database. + +You then created the products table in the remote database and deployed the application. + +You can use the same approach for your existing read heavy application to reduce read latencies and improve availability. If you are using an external platform to manage the content, you can connect the external platform to the D1 database, so that the content is automatically updated in the database. + +You can find the complete code for this tutorial in the [GitHub repository](https://github.com/harshil1712/e-com-d1-hono). + +