Pular para o conteúdo

Build a Comments API

Atualizado em Ver como Markdown

In this tutorial, you will use D1 and Hono to build a JSON API that stores and retrieves comments for a blog. You will create a D1 database, define a schema, and wire up GET and POST endpoints that read from and write to the database.

Prerequisites

  1. Sign up for a Cloudflare account.
  2. Install Node.js.

Node.js version manager

Use a Node version manager like Volta or nvm to avoid permission issues and change Node.js versions. Wrangler, discussed later in this guide, requires a Node version of 16.17.0 or later.

1. Create a new Worker project

  1. Create a new project named d1-comments-api by running:

    
    								
    									
    									npm
    									 create cloudflare@latest -- d1-comments-api
    								
    							

    For setup, select the following options:

    • For What would you like to start with?, choose Hello World example.
    • For Which template would you like to use?, choose Worker only.
    • For Which language do you want to use?, choose TypeScript.
    • For Do you want to use git for version control?, choose Yes.
    • For Do you want to deploy your application?, choose No (we will be making some changes before deploying).
  2. Move into the project directory:

    cd d1-comments-api

2. Install Hono

Install Hono, a lightweight web framework for building APIs on Workers:


								
									
									npm
									 i hono
								
							

3. Create a database

  1. Create a new D1 database with Wrangler:

    npx wrangler@latest d1 create d1-comments-api
  2. When prompted Would you like Wrangler to add it on your behalf?, select Yes. This automatically adds the DB binding to your Wrangler configuration file.

    Confirm that your Wrangler configuration file contains the d1_databases binding and the full project configuration:

    {
      "$schema": "./node_modules/wrangler/config-schema.json",
      "name": "d1-comments-api",
      "main": "src/index.ts",
      // Set this to today's date
      "compatibility_date": "2026-07-20",
      "d1_databases": [
        {
          "binding": "DB",
          "database_name": "d1-comments-api",
          "database_id": "<YOUR_DATABASE_ID>"
        }
      ]
    }
    name = "d1-comments-api"
    main = "src/index.ts"
    # Set this to today's date
    compatibility_date = "2026-07-20"
    
    [[d1_databases]]
    binding = "DB" # available in your Worker on env.DB
    database_name = "d1-comments-api"
    database_id = "<YOUR_DATABASE_ID>"

    Replace <YOUR_DATABASE_ID> with the ID output by the wrangler d1 create command.

Bindings allow your Workers to access resources, like D1 databases, KV namespaces, and R2 buckets, using a variable name in code. Your D1 database is accessible in your Worker on env.DB.

4. Create a schema and seed the database

  1. Create a schemas/schema.sql file with the following contents:

    DROP TABLE IF EXISTS comments;
    CREATE TABLE IF NOT EXISTS comments (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      author TEXT NOT NULL,
      body TEXT NOT NULL,
      post_slug TEXT NOT NULL
    );
    CREATE INDEX idx_comments_post_slug ON comments (post_slug);
    
    -- Optionally, uncomment the below query to insert seed data
    -- INSERT INTO comments (author, body, post_slug) VALUES ('Kristian', 'Great post!', 'hello-world');
  2. Run the schema against your local database first:

    npx wrangler d1 execute d1-comments-api --local --file schemas/schema.sql
  3. Verify the table was created locally:

    npx wrangler d1 execute d1-comments-api --local --command "SELECT name FROM sqlite_schema WHERE type = 'table'"
    ┌──────────┐
    │ name     │
    ├──────────┤
    │ comments │
    └──────────┘
  4. Once you are satisfied with the schema, apply it to your remote (production) database:

    npx wrangler d1 execute d1-comments-api --remote --file schemas/schema.sql

5. Initialize the Hono application

Replace the contents of src/index.ts with the following code. This sets up a Hono application with a typed Bindings interface so that env.DB is correctly typed as a D1Database:

import { Hono } from "hono";

const app = new Hono();

app.get("/api/posts/:slug/comments", async (c) => {
	// Do something and return an HTTP response
	// Optionally, do something with c.req.param("slug")
});

app.post("/api/posts/:slug/comments", async (c) => {
	// Do something and return an HTTP response
	// Optionally, do something with c.req.param("slug")
});

export default app;
import { Hono } from "hono";

type Bindings = {
	DB: D1Database;
};

const app = new Hono<{ Bindings: Bindings }>();

app.get("/api/posts/:slug/comments", async (c) => {
	// Do something and return an HTTP response
	// Optionally, do something with c.req.param("slug")
});

app.post("/api/posts/:slug/comments", async (c) => {
	// Do something and return an HTTP response
	// Optionally, do something with c.req.param("slug")
});

export default app;

6. Query comments

Add the logic for the GET endpoint to retrieve comments for a given post. This uses the D1 Workers Binding API to prepare and execute a parameterized query:

app.get("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { results } = await c.env.DB.prepare(
		"SELECT * FROM comments WHERE post_slug = ?",
	)
		.bind(slug)
		.run();
	return c.json(results);
});
app.get("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { results } = await c.env.DB.prepare(
		"SELECT * FROM comments WHERE post_slug = ?",
	)
		.bind(slug)
		.run();
	return c.json(results);
});

The code uses prepare to create a parameterized statement, bind to safely pass the slug value (preventing SQL injection), and run to execute the query.

7. Insert comments

Add the POST endpoint to create new comments. This validates the request body before inserting a row:

app.post("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { author, body } = await c.req.json();

	if (!author) return c.text("Missing author value for new comment", 400);
	if (!body) return c.text("Missing body value for new comment", 400);

	const { success } = await c.env.DB.prepare(
		"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
	)
		.bind(author, body, slug)
		.run();

	if (success) {
		c.status(201);
		return c.text("Created");
	} else {
		c.status(500);
		return c.text("Something went wrong");
	}
});
app.post("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { author, body } = await c.req.json<{
		author: string;
		body: string;
	}>();

	if (!author) return c.text("Missing author value for new comment", 400);
	if (!body) return c.text("Missing body value for new comment", 400);

	const { success } = await c.env.DB.prepare(
		"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
	)
		.bind(author, body, slug)
		.run();

	if (success) {
		c.status(201);
		return c.text("Created");
	} else {
		c.status(500);
		return c.text("Something went wrong");
	}
});

8. (Optional) Add CORS support

If you plan to call this API from a front-end application on a different origin, add CORS middleware. Import the cors module from Hono and add it before your routes:

import { Hono } from "hono";
import { cors } from "hono/cors";

const app = new Hono();
app.use("/api/*", cors());
import { Hono } from "hono";
import { cors } from "hono/cors";

type Bindings = {
	DB: D1Database;
};

const app = new Hono<{ Bindings: Bindings }>();
app.use("/api/*", cors());

When you make requests to /api/*, Hono will automatically generate and add CORS headers to responses from your API.

9. Deploy your application

  1. Log in to your Cloudflare account (if you have not already):

    npx wrangler whoami

    If you are not logged in, Wrangler will prompt you to log in.

  2. Deploy your Worker:

    npx wrangler deploy
  3. Test the API by inserting and then retrieving a comment:

    # Replace <YOUR_SUBDOMAIN> with your workers.dev subdomain
    curl -X POST https://d1-comments-api.<YOUR_SUBDOMAIN>.workers.dev/api/posts/hello-world/comments \
      -H "Content-Type: application/json" \
      -d '{"author": "Kristian", "body": "Great post!"}'
    Created
    curl https://d1-comments-api.<YOUR_SUBDOMAIN>.workers.dev/api/posts/hello-world/comments
    [
      {
        "id": 1,
        "author": "Kristian",
        "body": "Great post!",
        "post_slug": "hello-world"
      }
    ]

Full example

The complete src/index.ts with all routes and CORS support:

import { Hono } from "hono";
import { cors } from "hono/cors";

const app = new Hono();
app.use("/api/*", cors());

app.get("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { results } = await c.env.DB.prepare(
		"SELECT * FROM comments WHERE post_slug = ?",
	)
		.bind(slug)
		.run();
	return c.json(results);
});

app.post("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { author, body } = await c.req.json();

	if (!author) return c.text("Missing author value for new comment", 400);
	if (!body) return c.text("Missing body value for new comment", 400);

	const { success } = await c.env.DB.prepare(
		"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
	)
		.bind(author, body, slug)
		.run();

	if (success) {
		c.status(201);
		return c.text("Created");
	} else {
		c.status(500);
		return c.text("Something went wrong");
	}
});

export default app;
import { Hono } from "hono";
import { cors } from "hono/cors";

type Bindings = {
	DB: D1Database;
};

const app = new Hono<{ Bindings: Bindings }>();
app.use("/api/*", cors());

app.get("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { results } = await c.env.DB.prepare(
		"SELECT * FROM comments WHERE post_slug = ?",
	)
		.bind(slug)
		.run();
	return c.json(results);
});

app.post("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { author, body } = await c.req.json<{
		author: string;
		body: string;
	}>();

	if (!author) return c.text("Missing author value for new comment", 400);
	if (!body) return c.text("Missing body value for new comment", 400);

	const { success } = await c.env.DB.prepare(
		"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
	)
		.bind(author, body, slug)
		.run();

	if (success) {
		c.status(201);
		return c.text("Created");
	} else {
		c.status(500);
		return c.text("Something went wrong");
	}
});

export default app;

Next steps