Ship a Todo API With Tests You Will Actually Run

src/routes/todo.routes.js is empty.

You’re building a minimal todo API for your personal project, and you want to avoid the common pitfall of writing tests that get abandoned in a nested test directory you never open. This file will hold all four required routes, plus inline references to test cases that are easy to find and run every time you make a change.

Ship a Todo API With Tests You Will Actually Run desk detail
Desk detail for this page — not a measured lab photo.

Define core route schemas

You start by importing Zod and Express, then define a shared Todo schema at the top of the file that all routes use for validation. The schema requires a UUID string for the id field, a non-empty string for the content field, a boolean for the completed status that defaults to false, and a datetime string for the createdAt timestamp that defaults to the time of the request. You also define separate request schemas for POST and PATCH requests: the POST schema omits the id and createdAt fields (these are generated server-side) and the PATCH schema makes all fields optional, so users can update just the completed status, just the content, or both in a single request. Defining these schemas inline means you don’t have to import type definitions from a separate utils folder, and you can reference the exact same schemas in your test files to avoid mismatched validation rules between your code and your tests.

Write four required endpoints

Next, you initialize an Express router instance and add the four core todo routes that cover all standard CRUD operations for your todo list. First, the GET /api/todos endpoint fetches all todos stored in your in-memory database, and accepts an optional completed query parameter that filters results to only completed or incomplete todos depending on the boolean value passed. Second, the POST /api/todos endpoint validates incoming request bodies against the POST schema, generates a new UUID and createdAt timestamp for the new todo, adds it to the database, and returns the full todo object with a 201 Created status. Third, the PATCH /api/todos/:id endpoint first checks if a todo with the passed ID exists, returns a 404 Not Found status if it does not, validates the incoming update against the PATCH schema, merges the update with the existing todo, and returns the updated todo. Fourth, the DELETE /api/todos/:id endpoint checks for the existence of the requested todo, returns a 404 if it is not found, removes it from the database, and returns a 204 No Content status on success. Each route includes 1-line comments linking to the specific test case that covers its behavior, so you can jump directly to the relevant test if you make a change that breaks functionality later.

Attach test hooks inline

You store your test file at src/routes/todo.routes.test.js, directly adjacent to the route file, so you don’t have to navigate a nested test directory structure to find or update tests. You write four core test cases that cover all primary and edge case behavior for the routes, with each test explicitly referencing the inline comment in the route file it covers:

Illustrative card for Ship a Todo API With Tests You Will Actually Run
Illustrative worksheet for this topic. Treat numbers as examples.
  1. Send a POST request with a valid content string, confirm the response returns a 201 status, a valid UUID, the correct content, and a completed status of false.
  2. Send a PATCH request to the ID of an existing todo with a body of {completed: true}, confirm the response returns the updated todo with completed set to true, and a follow-up GET request returns the updated value.
  3. Send a DELETE request to a non-existent todo ID, confirm the response returns a 404 status, and the full list of todos remains unchanged.
  4. Send a GET request with the query parameter completed=true, confirm the response only returns todos where the completed field is set to true, and excludes all incomplete todos.

Illustrative example: 92% of developers run tests that are stored within 1 directory level of the code they modify, per internal Build Next Stack user surveys. Storing tests adjacent to the code they cover eliminates the biggest barrier to running tests regularly: having to hunt for test files across your repo. You also add a command to your package.json file: `test:todos`, which runs only the test file for the todo routes, instead of your entire test suite that can take minutes to run for larger projects.

Map test command to git pre-commit hook

You use Husky to set up a git pre-commit hook that triggers automatically every time you try to commit changes to your repo. You configure the hook to check if any modified files match `src/routes/todo.routes.js` or `src/routes/todo.routes.test.js`, and only run the `npm run test:todos` command if those files are changed. If the tests pass, the commit proceeds as normal. If any test fails, the commit aborts immediately, and you see an error message pointing to the specific test that failed, so you can fix the issue before you commit broken code. This setup means you never have to remember to run the tests manually: they run automatically exactly when you need them, before you push code to your repo, and they don’t add unnecessary wait time when you’re modifying unrelated parts of your codebase. You test the hook by intentionally breaking a route, trying to commit the change, and confirming the hook catches the failed test and aborts the commit before it is saved.

Save the complete working file

You save the full `todo.routes.js` file to your local directory, and confirm it works as expected by spinning up your local dev server with `npm run dev`. You send test requests to all four endpoints using your preferred API client: you create a new todo, mark it as complete, filter your todo list to only show completed items, then delete the todo when you’re done testing. All requests return the expected status codes and response bodies, and running the `npm run test:todos` command manually returns four passing tests with zero failures. The file is now fully functional, and you can deploy it to your production environment right away if you want, with confidence that all core functionality works as intended.

Open this file again to add priority sorting

The next time you open `src/routes/todo.routes.js`, you’ll add support for priority levels for todos, to let users rank tasks by urgency. You’ll add a new priority field to the core Todo schema, with allowed values of low, medium, and high, defaulting to medium if no value is provided. You’ll update the GET /api/todos endpoint to accept an optional `sortBy` query parameter that lets users sort todos by priority in either ascending or descending order. You’ll add two new test cases to the adjacent test file to confirm the sorting logic works as expected, and update the pre-commit hook configuration to include the new tests in the run when you modify the route file. The modular structure you built today means you won’t have to rewrite any existing routes or tests to add this new feature, and the test command will continue to run automatically every time you make changes to the file, so you can be confident your existing functionality doesn’t break when you add new features.

(Word count: 1328)