# Backend with TypeScript, PostgreSQL & Prisma: REST, Validation & Tests URL: https://www.prisma.io/blog/backend-prisma-typescript-orm-with-postgresql-rest-api-validation-dcba1ps7kip3 Published: 2020-08-20 Updated: 2026-07-03 Authors: Daniel Norman Series: Building a modern backend with TypeScript, PostgreSQL and Prisma Description: Build a REST API with TypeScript, PostgreSQL, Prisma ORM 7, and Hono, then add Zod validation and Vitest tests for CRUD endpoints. > **Update (July 2026):** This article was updated for Prisma ORM 7. The REST API is now built with [Hono](https://hono.dev/) instead of Hapi, with [Zod](https://zod.dev/) for input validation and [Vitest](https://vitest.dev/) for testing. It uses the rust-free `prisma-client` generator, the `prisma.config.ts` config file, and driver adapters. All commands and code work verbatim on Prisma ORM 7 with Node.js 20 or later. This article is part of a series on building a backend with TypeScript, PostgreSQL, and Prisma. In this second part, you will build a REST API on top of the data model from part 1, validate input, and test the API. ## Introduction [#introduction] The goal of the series is to explore and demonstrate different patterns, problems, and architectures for a modern backend by solving a concrete problem: **a grading system for online courses.** This is a good example because it features diverse relation types and is complex enough to represent a real-world use case. ### What the series covers [#what-the-series-covers] | Topic | Part | | ------------------------------ | ----------------------------------------------------------------------------------- | | Data modeling | [Part 1](https://www.prisma.io/blog/backend-prisma-typescript-orm-with-postgresql-data-modeling-tsjs1ps7kip1) | | CRUD | [Part 1](https://www.prisma.io/blog/backend-prisma-typescript-orm-with-postgresql-data-modeling-tsjs1ps7kip1) | | Aggregations | [Part 1](https://www.prisma.io/blog/backend-prisma-typescript-orm-with-postgresql-data-modeling-tsjs1ps7kip1) | | REST API layer | Part 2 (current) | | Validation | Part 2 (current) | | Testing | Part 2 (current) | | Authentication | Coming up | | Authorization | Coming up | | Integration with external APIs | Coming up | | Deployment | Coming up | ### What you will learn today [#what-you-will-learn-today] In the [first article](https://www.prisma.io/blog/backend-prisma-typescript-orm-with-postgresql-data-modeling-tsjs1ps7kip1), you designed a data model for the problem domain and wrote a seed script that uses Prisma Client to save data to the database. In this second article, you will build a REST API on top of that data model and Prisma schema. You will use [Hono](https://hono.dev/) to build the REST API. With the REST API, you'll be able to perform database operations via HTTP requests. As part of the REST API, you will develop the following aspects: 1. **REST API:** Implement an HTTP server with resource endpoints to handle CRUD for the different models. You will make Prisma Client available to the endpoint handlers through Hono middleware. 2. **Validation:** Add payload validation rules with Zod to ensure that user input matches the expected types of the Prisma schema. 3. **Testing:** Write tests for the REST endpoints with [Vitest](https://vitest.dev/) and Hono's [`app.request`](https://hono.dev/docs/guides/testing) helper, which simulates HTTP requests to verify the validation and persistence logic of the endpoints. By the end of this article you will have a REST API with endpoints for CRUD (Create, Read, Update, and Delete) operations and tests. The REST resources map HTTP requests to the models in the Prisma schema, for example a `GET /users` endpoint handles operations associated with the `User` model. > **Note:** Throughout the guide you'll find various **checkpoints** that enable you to validate whether you performed the steps correctly. ## Prerequisites [#prerequisites] ### Assumed knowledge [#assumed-knowledge] This series assumes basic knowledge of TypeScript, Node.js, and relational databases. Familiarity with REST concepts is useful. No prior knowledge of Prisma is required. This article continues directly from [part 1](https://www.prisma.io/blog/backend-prisma-typescript-orm-with-postgresql-data-modeling-tsjs1ps7kip1). You should already have the `grading-app` project from part 1, with the Prisma schema, a migrated database, and the generated Prisma Client. If you haven't completed part 1, do that first, as this article builds on its data model and project setup. ### Development environment [#development-environment] You should have the following installed: * [Node.js](https://nodejs.org/en/) 20 or later If you're using Visual Studio Code, the [Prisma extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma) is recommended for syntax highlighting, formatting, and autocomplete in your schema. You will use the [Prisma Postgres](https://www.prisma.io/postgres) database you created in part 1, so you don't need a local database server. If you set up a local database with Docker in part 1 instead, that works identically here. ## Building a REST API [#building-a-rest-api] Before diving into the implementation, here are some basic concepts relevant in the context of REST APIs: * **API:** Application programming interface. A set of rules that allow programs to talk to each other. Typically the developer creates the API on the server and allows clients to talk to it. * **REST:** A set of conventions that developers follow to expose state-related operations (in this case state stored in the database) over HTTP requests. As an example, check out the [GitHub REST API](https://docs.github.com/en/rest). * **Endpoint:** Entry point to the REST API which has the following properties (non-exhaustive): * **Path**, for example `/users/`, which is used to access the users endpoint. The path determines the URL used to access the endpoint, for example `www.myapi.com/users/`. * **HTTP method**, for example `GET`, `POST`, and `DELETE`. The HTTP method determines the type of operation an endpoint exposes, for example the `GET /users` endpoint allows fetching users and the `POST /users` endpoint allows creating users. * **Handler:** The code (in this case TypeScript) that handles requests for an endpoint. * **HTTP status codes:** The response HTTP status code informs the API consumer whether the operation was successful and if any errors occurred. Check out [this list](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) of the different HTTP status codes, for example `201` when a resource was created successfully, and `400` when consumer input fails validation. > **Note:** One of the key objectives of the REST approach is using HTTP as an application protocol to avoid reinventing the wheel by sticking to conventions. ### The API endpoints [#the-api-endpoints] The API will have the following endpoints (HTTP method followed by path): | Resource | HTTP Method | Route | Description | | ------------------ | ----------- | -------------------------------------------- | -------------------------------------------------------- | | `User` | `POST` | `/users` | Create a user (and optionally associate with courses) | | `User` | `GET` | `/users/{userId}` | Get a user | | `User` | `PUT` | `/users/{userId}` | Update a user | | `User` | `DELETE` | `/users/{userId}` | Delete a user | | `User` | `GET` | `/users` | Get users | | `CourseEnrollment` | `GET` | `/users/{userId}/courses` | Get a user's course enrollments | | `CourseEnrollment` | `POST` | `/users/{userId}/courses` | Enroll a user in a course (as student or teacher) | | `CourseEnrollment` | `DELETE` | `/users/{userId}/courses/{courseId}` | Delete a user's enrollment in a course | | `Course` | `POST` | `/courses` | Create a course | | `Course` | `GET` | `/courses` | Get courses | | `Course` | `GET` | `/courses/{courseId}` | Get a course | | `Course` | `PUT` | `/courses/{courseId}` | Update a course | | `Course` | `DELETE` | `/courses/{courseId}` | Delete a course | | `Test` | `POST` | `/courses/{courseId}/tests` | Create a test for a course | | `Test` | `GET` | `/courses/tests/{testId}` | Get a test | | `Test` | `PUT` | `/courses/tests/{testId}` | Update a test | | `Test` | `DELETE` | `/courses/tests/{testId}` | Delete a test | | `Test Result` | `GET` | `/users/{userId}/test-results` | Get a user's test results | | `Test Result` | `POST` | `/courses/tests/{testId}/test-results` | Create a test result for a test associated with a user | | `Test Result` | `GET` | `/courses/tests/{testId}/test-results` | Get multiple test results for a test | | `Test Result` | `PUT` | `/courses/tests/test-results/{testResultId}` | Update a test result (associated with a user and a test) | | `Test Result` | `DELETE` | `/courses/tests/test-results/{testResultId}` | Delete a test result | > **Note:** The paths containing a parameter enclosed in `{}`, for example `{userId}`, represent a variable that is interpolated in the URL. In `www.myapi.com/users/13` the `userId` is `13`. The endpoints above have been grouped based on the main model/resource they're associated with. The categorization helps with organizing the code into separate modules for maintainability. In this article, you will implement a subset of these endpoints (the CRUD operations for `User`) to illustrate the different patterns. You can implement the rest by following the same principles. > **Note:** Throughout the article, the words *endpoint* and *route* are used interchangeably. While they refer to the same thing, *endpoint* is the term used in the context of REST, while *route* is the term used in the context of HTTP servers. ### Hono [#hono] The API will be built with [Hono](https://hono.dev/), a small, fast web framework that runs on any JavaScript runtime (Node.js, Bun, Deno, Cloudflare Workers, and more). It has a clean routing API, first-class TypeScript support, built-in middleware, and a testing helper that lets you send requests to your app without starting a server. You will use the following packages alongside Hono: * `@hono/node-server` to run the Hono app on Node.js * `zod` for declarative, type-safe input validation * `@hono/zod-validator` to plug Zod schemas into Hono routes as validation middleware * `vitest` as the test runner Install them in your `grading-app` project: ```sh npm install hono @hono/node-server zod @hono/zod-validator dotenv npm install vitest --save-dev ``` ### Making Prisma Client available with middleware [#making-prisma-client-available-with-middleware] Before defining routes, you need a way to access Prisma Client inside your route handlers. Hono has the concept of [middleware](https://hono.dev/docs/guides/middleware): functions that run before your handlers and can read from and write to the request [context](https://hono.dev/docs/api/context). You will write a small middleware that attaches a single Prisma Client instance to the context so every handler can reach it through `c.get("prisma")`. Reusing one instance avoids opening a new database connection on every request. Create a new file `src/lib/prisma.ts`: ```ts import "dotenv/config"; import type { Context, Next } from "hono"; import { PrismaPg } from "@prisma/adapter-pg"; import { PrismaClient } from "../../generated/prisma/client"; const databaseUrl = process.env.DATABASE_URL; if (!databaseUrl) { throw new Error("DATABASE_URL is not set"); } const adapter = new PrismaPg({ connectionString: databaseUrl }); export const prisma = new PrismaClient({ adapter }); export type AppEnv = { Variables: { prisma: PrismaClient; }; }; export function withPrisma(c: Context, next: Next) { if (!c.get("prisma")) { c.set("prisma", prisma); } return next(); } ``` A few things to note: * Prisma Client is instantiated once with the `@prisma/adapter-pg` driver adapter, exactly as in the seed script from part 1. * `AppEnv` is a Hono [environment type](https://hono.dev/docs/api/hono#generics-for-hono). By declaring `Variables.prisma`, TypeScript knows that `c.get("prisma")` returns a `PrismaClient`, giving you autocomplete and type safety in every handler. * `withPrisma` sets the client on the context if it isn't already there, then calls `next()` to continue to the route handler. ### Creating the server [#creating-the-server] Next, create the Hono app and register the middleware. Splitting the app definition from the code that starts the server is deliberate: it lets your tests import the fully configured app and send requests to it without ever binding to a port. Create `src/app.ts`: ```ts import { Hono } from "hono"; import { withPrisma, type AppEnv } from "./lib/prisma"; import status from "./routes/status"; import users from "./routes/users"; const app = new Hono(); app.use("*", withPrisma); app.route("/", status); app.route("/", users); export default app; ``` `app.use("*", withPrisma)` applies the Prisma middleware to every route. `app.route()` mounts route modules (which you'll create next) onto the app, keeping each resource's routes in its own file. Then create the runnable entry point, `src/index.ts`, which starts the server with `@hono/node-server`: ```ts import { serve } from "@hono/node-server"; import app from "./app"; serve( { fetch: app.fetch, port: 3000, }, (info) => { console.log(`Server is running on http://localhost:${info.port}`); }, ); ``` Finally, add `dev`, `start`, and `test` scripts to your `package.json`: ```json { "scripts": { "dev": "tsx watch src/index.ts", "start": "tsx src/index.ts", "test": "vitest run" } } ``` ### Defining the status route [#defining-the-status-route] Before defining routes related to business logic, it's good practice to add a status endpoint that returns a `200` HTTP status code. This is useful to check that the server is running correctly. Create the `src/routes` directory and add a `status.ts` file: ```ts import { Hono } from "hono"; import type { AppEnv } from "../lib/prisma"; const status = new Hono(); status.get("/", (c) => { return c.json({ up: true }, 200); }); export default status; ``` Here you create a Hono instance for the status routes, define a `GET /` handler that returns the object `{ up: true }` with a `200` status code, and export it so `app.ts` can mount it. Start the server to try it out: ```sh npm run dev ``` **Checkpoint:** If you open [http://localhost:3000](http://localhost:3000) in your browser, you should see: `{"up":true}` ### Testing the status route [#testing-the-status-route] To test the status endpoint, you'll use [Vitest](https://vitest.dev/) as the test runner together with Hono's [`app.request`](https://hono.dev/docs/guides/testing) helper, which sends a request straight to your app and returns a standard `Response`. Because `app.request` doesn't start a server, tests run fast and don't need a free port. Create a `tests` directory and add `status.test.ts`: ```ts import { describe, test, expect } from "vitest"; import app from "../src/app"; describe("Status route", () => { test("GET / returns 200 and { up: true }", async () => { const res = await app.request("/"); expect(res.status).toBe(200); expect(await res.json()).toEqual({ up: true }); }); }); ``` The test imports the configured `app`, sends a `GET /` request with `app.request`, and asserts on the status code and the JSON body. **Checkpoint:** Run the tests with `npm test`. You should see output similar to: ```sh ✓ tests/status.test.ts > Status route > GET / returns 200 and { up: true } Test Files 1 passed (1) Tests 1 passed (1) ``` ### Defining the user routes [#defining-the-user-routes] You'll now create a route module for the `User` resource. Because the Prisma middleware is registered on the app, every handler in this module can access Prisma Client through `c.get("prisma")`. Create `src/routes/users.ts` with the following starting point: ```ts import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { z } from "zod"; import type { AppEnv } from "../lib/prisma"; const users = new Hono(); export default users; ``` The `Hono` type parameter carries the `prisma` context variable through to every handler you add. In the next steps you'll fill in the CRUD routes. ### Defining the create user route with validation [#defining-the-create-user-route-with-validation] The create user route uses the HTTP method `POST` and the path `/users`. User input arrives at runtime, so it can't be checked by the TypeScript compiler. This is what validation is for: it's a runtime check that the incoming payload has the right shape before you touch the database. You'll define the expected shape once as a Zod schema and reuse it both for validation and for deriving the TypeScript type. Add the schemas to `src/routes/users.ts`: ```ts const userSchema = z.object({ firstName: z.string(), lastName: z.string(), email: z.email(), social: z .object({ bluesky: z.string().optional(), linkedin: z.string().optional(), website: z.string().optional(), }) .optional(), }); ``` Now define the create route. `zValidator("json", userSchema)` is middleware from `@hono/zod-validator` that validates the JSON body against `userSchema`. If validation fails, it responds with a `400` automatically. If it succeeds, the parsed and typed payload is available through `c.req.valid("json")`: ```ts users.post("/users", zValidator("json", userSchema), async (c) => { const prisma = c.get("prisma"); const data = c.req.valid("json"); const createdUser = await prisma.user.create({ data, select: { id: true }, }); return c.json(createdUser, 201); }); ``` Here you access `prisma` from the context (set by the middleware), pass the validated payload to `prisma.user.create`, and return the created user's `id` with a `201` status code. Because `social` in the schema matches the `Json?` field in the Prisma schema, you pass the object straight through, no manual serialization required. ### Testing the create user route [#testing-the-create-user-route] Create `tests/users.test.ts` and add a test that sends a valid payload and asserts that the response contains a numeric `id`: ```ts import { describe, test, expect, afterAll } from "vitest"; import app from "../src/app"; import { prisma } from "../src/lib/prisma"; describe("User routes", () => { let userId: number; afterAll(async () => { await prisma.$disconnect(); }); test("POST /users creates a user", async () => { const res = await app.request("/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ firstName: "test-first-name", lastName: "test-last-name", email: `test-${Date.now()}@prisma.io`, social: { bluesky: "thisisalice", website: "https://thisisalice.com" }, }), }); expect(res.status).toBe(201); const body = await res.json(); userId = body.id; expect(typeof userId).toBe("number"); }); }); ``` The test stores the created `userId` in a variable scoped to the `describe` block so later tests can reuse it. Using `` `test-${Date.now()}@prisma.io` `` keeps the email unique on every run, avoiding unique constraint errors. The `afterAll` hook disconnects Prisma Client when the suite finishes. Now add a test for the validation logic by sending an invalid payload that omits the required `firstName` field: ```ts test("POST /users rejects invalid input with 400", async () => { const res = await app.request("/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ lastName: "test-last-name", email: `test-${Date.now()}@prisma.io`, }), }); expect(res.status).toBe(400); }); ``` **Checkpoint:** Run `npm test` and verify that both tests pass. ### Defining and testing the get user route [#defining-and-testing-the-get-user-route] The get user endpoint has the `GET /users/{userId}` signature. The practice of first writing the test and then the implementation is often referred to as *test-driven development*. It can improve productivity by giving you a fast way to verify the correctness of changes while you work on the implementation. #### Writing the tests [#writing-the-tests] First, test that the route returns `404` when a user is not found. Add these tests inside the `describe` block in `users.test.ts`: ```ts test("GET /users/:userId returns 404 for a non-existent user", async () => { const res = await app.request("/users/999999"); expect(res.status).toBe(404); }); test("GET /users/:userId returns the user", async () => { const res = await app.request(`/users/${userId}`); expect(res.status).toBe(200); const user = await res.json(); expect(user.id).toBe(userId); }); ``` The second test uses the `userId` from the create test, so it fetches an existing user. #### Defining the route [#defining-the-route] To validate URL parameters, define a Zod schema for the `userId` and use `zValidator("param", ...)`. Since parameters arrive as strings, `z.coerce.number()` converts the value before checking that it's an integer: ```ts const userIdParam = z.object({ userId: z.coerce.number().int(), }); users.get("/users/:userId", zValidator("param", userIdParam), async (c) => { const prisma = c.get("prisma"); const { userId } = c.req.valid("param"); const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { return c.body(null, 404); } return c.json(user, 200); }); ``` The handler parses the validated `userId`, queries the database, returns `404` if no user is found, and otherwise returns the user with a `200`. > **Note:** When calling `findUnique`, Prisma Client returns `null` if no record could be found. **Checkpoint:** Run `npm test` and verify that all tests pass. ### Defining and testing the delete user route [#defining-and-testing-the-delete-user-route] The delete user endpoint has the `DELETE /users/{userId}` signature. #### Writing the tests [#writing-the-tests-1] First, write a test for the parameter validation, then one for the delete logic that removes the user created earlier: ```ts test("DELETE /users/:userId fails with 400 for an invalid id", async () => { const res = await app.request("/users/aa22", { method: "DELETE" }); expect(res.status).toBe(400); }); test("DELETE /users/:userId deletes the user", async () => { const res = await app.request(`/users/${userId}`, { method: "DELETE" }); expect(res.status).toBe(204); }); ``` > **Note:** The `204` status code indicates that the request succeeded but the response has no content. #### Defining the route [#defining-the-route-1] Reuse the `userIdParam` schema for validation and add the route: ```ts users.delete("/users/:userId", zValidator("param", userIdParam), async (c) => { const prisma = c.get("prisma"); const { userId } = c.req.valid("param"); await prisma.user.delete({ where: { id: userId } }); return c.body(null, 204); }); ``` **Checkpoint:** Run `npm test` and verify that all tests pass. ### Defining and testing the update user route [#defining-and-testing-the-update-user-route] The update user endpoint has the `PUT /users/{userId}` signature. Because the delete test above removes the user, add the update tests *before* the delete tests in the file so the user still exists when the update runs. #### Writing the tests [#writing-the-tests-2] ```ts test("PUT /users/:userId fails with 400 for an invalid id", async () => { const res = await app.request("/users/aa22", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ firstName: "x" }), }); expect(res.status).toBe(400); }); test("PUT /users/:userId updates the user", async () => { const res = await app.request(`/users/${userId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ firstName: "test-first-name-UPDATED", lastName: "test-last-name-UPDATED", }), }); expect(res.status).toBe(200); const user = await res.json(); expect(user.firstName).toBe("test-first-name-UPDATED"); expect(user.lastName).toBe("test-last-name-UPDATED"); }); ``` #### Deriving the update validation from the create schema [#deriving-the-update-validation-from-the-create-schema] For the update route, every field should be optional so you can update a single field, for example just `firstName`. Rather than write a second schema by hand, derive it from `userSchema` with Zod's [`.partial()`](https://zod.dev/api?id=partial) method, which makes every field optional. This keeps the create and update rules in sync and your code [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself): ```ts const updateUserSchema = userSchema.partial(); ``` #### Defining the route [#defining-the-route-2] The update route validates both the `userId` parameter and the JSON body, then passes the validated payload to `prisma.user.update`: ```ts users.put( "/users/:userId", zValidator("param", userIdParam), zValidator("json", updateUserSchema), async (c) => { const prisma = c.get("prisma"); const { userId } = c.req.valid("param"); const data = c.req.valid("json"); const updatedUser = await prisma.user.update({ where: { id: userId }, data, }); return c.json(updatedUser, 200); }, ); ``` **Checkpoint:** Run `npm test`. With all four user routes and the status route in place, you should see all tests pass: ```sh ✓ tests/status.test.ts > Status route > GET / returns 200 and { up: true } ✓ tests/users.test.ts > User routes > POST /users creates a user ✓ tests/users.test.ts > User routes > POST /users rejects invalid input with 400 ✓ tests/users.test.ts > User routes > GET /users/:userId returns 404 for a non-existent user ✓ tests/users.test.ts > User routes > GET /users/:userId returns the user ✓ tests/users.test.ts > User routes > PUT /users/:userId fails with 400 for an invalid id ✓ tests/users.test.ts > User routes > PUT /users/:userId updates the user ✓ tests/users.test.ts > User routes > DELETE /users/:userId fails with 400 for an invalid id ✓ tests/users.test.ts > User routes > DELETE /users/:userId deletes the user Test Files 2 passed (2) Tests 9 passed (9) ``` ## Summary and next steps [#summary-and-next-steps] The article covered a lot of ground, starting with REST concepts and then going into Hono concepts such as routes, middleware, route modules, testing, and validation. You made Prisma Client available throughout your application with a middleware, and implemented routes that use it. You validated input at the edge with Zod, deriving the update schema from the create schema so the rules stay in sync, and you tested every endpoint with Vitest and Hono's `app.request` helper. Throughout, TypeScript kept the handlers in sync with the database schema, so the types you query against are the same types Prisma generates from your models. The article covered a subset of all the endpoints. As a next step, you could implement the other routes following the same principles. While Prisma aims to make working with relational databases easy, it helps to have a deeper understanding of the underlying database. Check out [Prisma's Data Guide](https://www.prisma.io/dataguide) to learn more about how databases work. In the next parts of this series, you'll learn more about: * Authentication: implementing passwordless authentication with email and JWT * Continuous integration: building a GitHub Actions pipeline to automate testing of the backend * Integration with external APIs: using a transactional email API to send emails * Authorization: providing different levels of access to different resources * Deployment Your database runs on [Prisma Postgres](https://www.prisma.io/postgres), which you can manage in the [Prisma Console](https://console.prisma.io). If AI agents are part of your development workflow, the [Prisma MCP server](https://www.prisma.io/docs/ai/tools/mcp-server) lets them create and manage databases as they need them. When you reach the deployment part of this series, [Prisma Compute](https://www.prisma.io/compute) runs your backend on the same platform as your database. This series teaches Prisma ORM 7. If you want to see where Prisma ORM is heading for AI-assisted development, read [The Next Evolution of Prisma ORM](https://www.prisma.io/blog/the-next-evolution-of-prisma-orm). Prisma ORM 8, the version npm installs by default, is the agent-native evolution of the ORM. The type-safe, schema-as-contract approach you used here to keep validation and queries in sync is exactly what Prisma 8 builds on: every query returns structured output that humans and coding agents can build on safely, and you can adopt it incrementally alongside an existing project. The [performance benchmark](https://www.prisma.io/blog/prisma-next-performance-benchmark) covers what the rewrite means for throughput and client size.