惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cloudbric
Cloudbric
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
A
About on SuperTechFans
宝玉的分享
宝玉的分享
小众软件
小众软件
T
Tor Project blog
The Hacker News
The Hacker News
WordPress大学
WordPress大学
IT之家
IT之家
L
LINUX DO - 热门话题
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Latest news
Latest news
Martin Fowler
Martin Fowler
F
Full Disclosure
爱范儿
爱范儿
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
W
WeLiveSecurity
C
Cisco Blogs
Recorded Future
Recorded Future
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - Franky
美团技术团队
N
Netflix TechBlog - Medium
Know Your Adversary
Know Your Adversary
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Help Net Security
雷峰网
雷峰网
G
Google Developers Blog
人人都是产品经理
人人都是产品经理
Microsoft Azure Blog
Microsoft Azure Blog
Security Latest
Security Latest
M
MIT News - Artificial intelligence
J
Java Code Geeks
Project Zero
Project Zero
Jina AI
Jina AI
P
Palo Alto Networks Blog
Vercel News
Vercel News
腾讯CDC
N
News | PayPal Newsroom
V
Visual Studio Blog
Cisco Talos Blog
Cisco Talos Blog
V
Vulnerabilities – Threatpost
AWS News Blog
AWS News Blog

Deno

Deno 2.8 | Deno Claw Patrol: an open-source security firewall for agents | Deno Fresh 2.3: Zero JS by default, View Transitions, and Temporal support | Deno Deno 2.7: Temporal API, Windows ARM, and npm overrides | Deno Build a dinosaur runner game with Deno, pt. 6 | Deno Build a dinosaur runner game with Deno, pt. 5 | Deno Deno Deploy is Generally Available | Deno Introducing Deno Sandbox | Deno Build a dinosaur runner game with Deno, pt. 4 | Deno Build a dinosaur runner game with Deno, pt. 3 | Deno Build a dinosaur runner game with Deno, pt. 2 | Deno React / Next.js Denial-of-Service Vulnerability: Deno Deploy users protected | Deno Deno 2.6: dx is the new npx | Deno Build a dinosaur runner game with Deno, pt. 1 | Deno React Server Functions / Next.js Vulnerability: Deno Deploy users protected | Deno My highlights from the new Deno Deploy | Deno Deno's Other Open Source Projects | Deno How Deno protects against npm exploits | Deno Help Us Raise $200k to Free JavaScript from Oracle | Deno Deno 2.5: Permissions in the config file | Deno Fresh 2.0 Graduates to Beta, Adds Vite Support | Deno Deno 2.4: deno bundle is back | Deno JavaScript™ Trademark Update | Deno What's coming to JavaScript | Deno A brief history of JavaScript | Deno Reports of Deno's Demise Have Been Greatly Exaggerated | Deno An Update on Fresh | Deno How Plaid migrated 100 services to a new database platform 5x faster with Deno | Deno Deno 2.3: Improved deno compile, local npm packages, and more | Deno Add JSR packages with pnpm and Yarn | Deno Zero-config Debugging with Deno and OpenTelemetry | Deno Exploring Art with TypeScript, Jupyter, Polars, and Observable Plot | Deno Deno v Oracle Update 3: Fighting the JavaScript Trademark | Deno Build a custom RAG AI agent in TypeScript and Jupyter | Deno How to get deep traces in your Node.js backend with OTel and Deno | Deno toranoana.deno #20 登録受付中(2025年3月14日) | Deno Node just added TypeScript support. What does that mean for Deno? | Deno The Dino 🦕, the Llama 🦙, and the Whale 🐋 | Deno Publish a lint rule, get a prize | Deno Deno 2.2: OpenTelemetry, Lint Plugins, node:sqlite | Deno If you're not using npm specifiers, you're doing it wrong | Deno How Deno's documentation is evolving | Deno Oracle justified its JavaScript trademark with Node.js—now it wants that ignored | Deno Introducing the JSR open governance board | Deno Intro to Wasm in Deno | Deno Announcing OpenAI on JSR | Deno Deno in 2024 | Deno Goodbye WinterCG, welcome WinterTC | Deno Build a SolidJS app with Deno | Deno Run your Next.js SSR app on Deno Deploy | Deno Solve Advent of Code 2024 with Deno and Win Prizes! | Deno Deno v. Oracle: Canceling the JavaScript Trademark | Deno Deno 2.1: Wasm Imports and other enhancements | Deno Build a Typesafe API with tRPC and Deno | Deno Self-contained Executable Programs with Deno Compile | Deno Build a Database App with Drizzle ORM and Deno | Deno Introducing your new JavaScript package manager: Deno | Deno Announcing Growthbook on JSR | Deno Build an Astro site with Deno | Deno How to convert CommonJS to ESM | Deno Announcing Deno 2 | Deno The Final Touches: What’s New In v2.0.0-rc.10 | Deno Announcing Stable V8 Bindings for Rust | Deno Deno 2.0 Release Candidate | Deno Secure, efficient private npm registries with Cloudsmith and Deno | Deno Painting the Plane as We Fly It: Designing JSR | Deno Introducing Web Cache API support on Deno Deploy | Deno Deno 1.46: The Last 1.x Release | Deno Protect your cloud spend with new Deno Deploy spend limits | Deno What we got wrong about HTTP imports | Deno Benchmarking AWS Lambda Cold Starts Across JavaScript Runtimes | Deno Announcing Supabase on JSR | Deno Deno 1.45: Workspace and Monorepo Support | Deno Introducing KV Backup for Deno Subhosting | Deno A Gentle Intro to TypeScript | Deno Announcing Hono on JSR | Deno How We Made the Deno Language Server Ten Times Faster | Deno How the Guardian uses Deno to audit accessibility and performance across their 2.7 million articles | Deno Introducing More Flexible Domain Association for Deno Subhosting | Deno The stabilization process of the Standard Library has begun | Deno Deno 1.44: Private npm registries, improved Node.js compat, and performance boosts | Deno How we built a secure, performant, multi-tenant cloud platform to run untrusted code | Deno The Deno Standard Library is now available on JSR | Deno How to document your JavaScript package | Deno Your Low Code Solution Needs an Escape Hatch | Deno Deno 1.43: Improved Language Server performance | Deno How Slack used Deno to save months of engineering effort in launching their new platform | Deno JSR Is Not Another Package Manager | Deno Announcing the Hookdeck SDK on JSR | Deno Announcing the Neon Serverless Driver on JSR | Deno An intro to TSConfig for JavaScript Developers | Deno How we built JSR | Deno How Netlify used Deno Subhosting to build a successful edge functions product | Deno Introducing Simpler Project Creation in Deno Deploy | Deno Deno 1.42: Better dependency management with JSR | Deno Introducing deployctl, the command line interface for Deno Deploy | Deno Introducing JSR - the JavaScript Registry | Deno How to add Monaco to a Next.js app and securely run untrusted user code | Deno Survey Results and Roadmap | Deno Deno 1.41: smaller deno compile binaries | Deno
How to Build a CRUD API with Oak and Deno KV | Deno
Andy Jiang · 2023-06-01 · via Deno

Deno KV is one of the first databases that is built right into the runtime. This means you don’t need to do any extra steps such as provisioning a database or copying and pasting API keys to build stateful applications. To open a connection to a data store, you can simply write:

const kv = await Deno.openKv();

In addition to being a key-value store with a simple, yet flexible API, it’s a production-ready database with atomic transactions, consistency control, and cutting edge performance.

Through this introductory tutorial, you’ll learn how to use Deno KV to build a simple stateful CRUD API written in Oak. We’ll cover:

  • Secondary indexes
  • Atomic transactions
  • List and pagination

Before we get started, Deno KV is currently available with the --unstable flag in Deno 1.33 and up. If you’re interested in using Deno KV on Deno Deploy, please join the waitlist as it’s still in closed beta.

Follow along below or check out the source code.

Set up the database models

This API is fairly simple and use two models, where each user will have an optional address.

A user and address model for this simple API

In a new repo, create a db.ts file, which will contain all the information and logic for the database. Let’s start with type definitions:

export interface User {
  id: string;
  email: string;
  name: string;
  password: string;
}

export interface Address {
  city: string;
  street: string;
}

Create API routes

Next, let’s create API routes with the following functions:

  • Upserting a user
  • Upserting an address tied to a user
  • Listing all users
  • List a single user by ID
  • List a single user by email
  • List an address by a user’s ID
  • Delete a user and any associated address

We can do this easily with Oak (inspired by Koa) which comes with its own Router.

Let’s create a new file main.ts and add the following routes. We’ll keep some of the logic in the route handlers blank for now:

import {
  Application,
  Context,
  helpers,
  Router,
} from "https://deno.land/x/oak@v12.4.0/mod.ts";

const { getQuery } = helpers;
const router = new Router();

router
  .get("/users", async (ctx: Context) => {
  })
  .get("/users/:id", async (ctx: Context) => {
    const { id } = getQuery(ctx, { mergeParams: true });
  })
  .get("/users/email/:email", async (ctx: Context) => {
    const { email } = getQuery(ctx, { mergeParams: true });
  })
  .get("/users/:id/address", async (ctx: Context) => {
    const { id } = getQuery(ctx, { mergeParams: true });
  })
  .post("/users", async (ctx: Context) => {
    const body = ctx.request.body();
    const user = await body.value;
  })
  .post("/users/:id/address", async (ctx: Context) => {
    const { id } = getQuery(ctx, { mergeParams: true });
    const body = ctx.request.body();
    const address = await body.value;
  })
  .delete("/users/:id", async (ctx: Context) => {
    const { id } = getQuery(ctx, { mergeParams: true });
  });

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });

Next, let’s dive into Deno KV by writing database functions.

Deno KV

Back to our db.ts file, let’s start adding the database helper functions beneath the type definitions.

const kv = await Deno.openKv();

export async function getAllUsers() {
}

export async function getUserById(id: string): Promise<User> {
}

export async function getUserByEmail(email: string) {
}

export async function getAddressByUserId(id: string) {
}

export async function upsertUser(user: User) {
}

export async function updateUserAndAddress(user: User, address: Address) {
}

export async function deleteUserById(id: string) {
}

Let’s start by filling out getUserById:

export async function getUserById(id: string): Promise<User> {
  const key = ["user", id];
  return (await kv.get<User>(key)).value!;
}

This is relatively simple and we use the key prefix "user" and an id with kv.get().

But how do we add getUserByEmail?

Add a secondary index

A secondary index is an index that’s not a primary index and may contain duplicates. In this case, our secondary index is email.

Since Deno KV is a simple key value store, we’ll create a second key prefix, "user_by_email", which uses email to create the key and returns the associated user id. Here’s an example:

const user = (await kv<User>.get(["user", "1"])).value!;







const id = (await kv.get(["user_by_email", "andy@deno.com"])).value;

Then, in order to grab the user, we’ll perform a separate kv.get() on the first index.

With both these indexes, we can now write getUserByEmail:

export async function getUserByEmail(email: string) {
  const userByEmailKey = ["user_by_email", email];
  const id = (await kv.get(userByEmailKey)).value as string;
  const userKey = ["user", id];
  return (await kv<User>.get(userKey)).value!;
}

Now, when we upsertUser, we’ll have to update the user in the "user" primary key prefix. If email is different, then we’ll also have to update the secondary key prefix, "user_by_email".

But how do we ensure that our data doesn’t fall out of sync when both update transactions happen at the same time?

Use atomic transactions

We’ll use kv.atomic(), which guarantee that either all operations within the transaction are successfully completed, or the transaction is rolled back to its initial state in the event of a failure, leaving the database unchanged.

Here’s how we define upsertUser:

export async function upsertUser(user: User) {
  const userKey = ["user", user.id];
  const userByEmailKey = ["user_by_email", user.email];

  const oldUser = await kv.get<User>(userKey);

  if (!oldUser.value) {
    const ok = await kv.atomic()
      .check(oldUser)
      .set(userByEmailKey, user.id)
      .set(userKey, user)
      .commit();
    if (!ok) throw new Error("Something went wrong.");
  } else {
    const ok = await kv.atomic()
      .check(oldUser)
      .delete(["user_by_email", oldUser.value.email])
      .set(userByEmailKey, user.id)
      .set(userKey, user)
      .commit();
    if (!ok) throw new Error("Something went wrong.");
  }
}

We first get oldUser to check whether it exists. If not, .set() the key prefixes "user" and "user_by_email" with user and user.id. Otherwise, since the user.email may have changed, we remove the value at the "user_by_email" by deleting the value at key ["user_by_email", oldUser.value.email].

We do all of this with .check(oldUser) to ensure another client hasn’t changed the values. Otherwise, we are susceptible to a race condition where the wrong record could be updated. If the .check() passes and the values remain unchanged, we can then complete the transaction with .set() and .delete().

kv.atomic() is a great way to ensure correctness when multiple clients are sending write transactions, such as in banking/finance and other data sensitive applications.

List and pagination

Next, let’s define getAllUsers. We can do this with kv.list(), which returns a key iterator that we can enumerate to get the values, which we .push() into users array:

export async function getAllUsers() {
  const users = [];
  for await (const res of kv.list({ prefix: ["user"] })) {
    users.push(res.value);
  }
  return users;
}

Note that this simple function iterates through and returns the entirety of the KV store. If this API were interacting with a frontend, we could pass a { limit: 50 } option to retrieve the first 50 items:

let iter = await kv.list({ prefix: ["user"] }, { limit: 50 });

And when the user wants more data, retrieve the next batch using iter.cursor:

iter = await kv.list({ prefix: ["user"] }, { limit: 50, cursor: iter.cursor });

Add second model, Address

Let’s add the second model, Address, to our database. We’ll use a new key prefix, "user_address" followed by the identifier user_id (["user_address", user_id]) to serve as a “join” between these two KV subspaces.

Now, let’s write our getAddressByUser function:

export async function getAddressByUserId(id: string) {
  const key = ["user_address", id];
  return (await kv<Address>.get(key)).value!;
}

And we can write our updateUserAndAddress function. Note that we’ll need to use kv.atomic() since we want to update three KV entries with key prefixes of "user", "user_by_email", and "user_address".

export async function updateUserAndAddress(user: User, address: Address) {
  const userKey = ["user", user.id];
  const userByEmailKey = ["user_by_email", user.email];
  const addressKey = ["user_address", user.id];

  const oldUser = await kv.get<User>(userKey);

  if (!oldUser.value) {
    const ok = await kv.atomic()
      .check(oldUser)
      .set(userByEmailKey, user.id)
      .set(userKey, user)
      .set(addressKey, address)
      .commit();
    if (!ok) throw new Error("Something went wrong.");
  } else {
    const ok = await kv.atomic()
      .check(oldUser)
      .delete(["user_by_email", oldUser.value.email])
      .set(userByEmailKey, user.id)
      .set(userKey, user)
      .set(addressKey, address)
      .commit();
    if (!ok) throw new Error("Something went wrong.");
  }
}

Add kv.delete()

Finally, in order to round out the CRUD functionality of our app, let’s define deleteByUserId.

Similar to the other mutation functions, we’ll retrieve userRes and use .atomic().check(userRes) before .delete()-ing the three keys:

export async function deleteUserById(id: string) {
  const userKey = ["user", id];
  const userRes = await kv.get(userKey);
  if (!userRes.value) return;
  const userByEmailKey = ["user_by_email", userRes.value.email];
  const addressKey = ["user_address", id];

  await kv.atomic()
    .check(userRes)
    .delete(userKey)
    .delete(userByEmailKey)
    .delete(addressKey)
    .commit();
}

Update route handlers

Now that we’ve defined the database functions, let’s import them in main.ts and fill out the rest of the functionality in our route handlers. Here’s the full main.ts file:

import {
  Application,
  Context,
  helpers,
  Router,
} from "https://deno.land/x/oak@v12.4.0/mod.ts";
import {
  deleteUserById,
  getAddressByUserId,
  getAllUsers,
  getUserByEmail,
  getUserById,
  updateUserAndAddress,
  upsertUser,
} from "./db.ts";

const { getQuery } = helpers;
const router = new Router();

router
  .get("/users", async (ctx: Context) => {
    ctx.response.body = await getAllUsers();
  })
  .get("/users/:id", async (ctx: Context) => {
    const { id } = getQuery(ctx, { mergeParams: true });
    ctx.response.body = await getUserById(id);
  })
  .get("/users/email/:email", async (ctx: Context) => {
    const { email } = getQuery(ctx, { mergeParams: true });
    ctx.response.body = await getUserByEmail(email);
  })
  .get("/users/:id/address", async (ctx: Context) => {
    const { id } = getQuery(ctx, { mergeParams: true });
    ctx.response.body = await getAddressByUserId(id);
  })
  .post("/users", async (ctx: Context) => {
    const body = ctx.request.body();
    const user = await body.value;
    await upsertUser(user);
  })
  .post("/users/:id/address", async (ctx: Context) => {
    const { id } = getQuery(ctx, { mergeParams: true });
    const body = ctx.request.body();
    const address = await body.value;
    const user = await getUserById(id);
    await updateUserAndAddress(user, address);
  })
  .delete("/users/:id", async (ctx: Context) => {
    const { id } = getQuery(ctx, { mergeParams: true });
    await deleteUserById(id);
  });

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });

Test our API

Let’s run our application and test it. To run it:

deno run --allow-net --watch --unstable main.ts

We can test out our application with CURLs. Let’s add a new user:

curl -X POST http://localhost:8000/users -H "Content-Type: application/json" -d '{ "id": "1", "email": "andy@deno.com", "name": "andy", "password": "12345" }'

When we point our browser to localhost:8000/users, we should see:

JSON response of our new user

Let’s see if we can retrieve the user by email by pointing our browser to localhost:8000/users/email/andy@deno.com:

JSON response of our new user by email

Let’s send a POST request to add an address to this user:

curl -X POST http://localhost:8000/users/1/address -H "Content-Type: application/json" -d '{ "city": "los angeles", "street": "main street" }'

And let’s see if that worked by going to localhost:8000/users/1/address:

JSON response of the address of our new user

Let’s update the same user with id 1 with a new name:

curl -X POST http://localhost:8000/users -H "Content-Type: application/json" -d '{ "id": "1", "email": "andy@deno.com", "name": "an even better andy", "password": "12345" }'

And we can see that change reflected in our browser at localhost:8000/users/1:

JSON response of an updated user

Finally, let’s delete the user:

curl -X DELETE http://localhost:8000/users/1

When we point our browser to localhost:8000/users, we should see nothing:

No more users left

What’s next

This is just an introduction to building stateful APIs with Deno KV, but hopefully you can see how quick and easy it is to get started.

With this CRUD API, you can create a simple frontend client to interact with the data.

Don’t miss any updates — follow us on Twitter.