Building a Scalable REST API with TypeScript, Express, Drizzle ORM, and Turso Database: A Step-by-Step Guide
Tags: typescript, express, drizzle, turso
Published: 2024-05-27 13:57:44

Building REST APIs with Express.js is straightforward and a must-have skill for every web developer. In this guide, we'll learn a step-by-step approach to building a REST API. We'll use the following technologies to build an API for a note-taking application:

- **TypeScript**: Literally JavaScript with static typing. It compiles down to JavaScript. Suitable for building large-scale JavaScript applications with confidence.
- **Express**: A minimalist, unopinionated Node.js framework for building server-side applications.
- **Drizzle**: An SQL Object-Relational Mapping (ORM) tool that makes it easy to interact with the database.
- **Turso**: A fast and scalable SQLite database technology for building production-ready applications. Easy to set up and has a generous free tier.

## Why Learn Express?

Using opinionated Node.js frameworks such as Nest.js and Sails.js is great, especially for large-scale applications. However, they're not ideal for beginners because they abstract away a lot of how the server actually works. In fact, Nest.js is an abstraction on top of Express.js.

Express.js, on the other hand, is unopinionated and allows for more flexibility and a better learning experience. It's lightweight and easy to get started with, especially if you're coming from frontend JavaScript. Moreover, understanding Express will give you the knowledge and confidence to pick up other Node.js backend frameworks.

## What the Guide Covers

We will cover the following topics in this article:

- How to set up a TypeScript Project with Express
- How Express middlewares work
- Setting up Turso database
- Connecting and interacting with Turso database using Drizzle ORM
- Creating a CRUD (Create Read Update Update Delete) API
- How to validate and sanitize data using the third-party `express-validator` middlewares
- Testing an API with Thunder Client VS Code extension

## Prerequisites

To effortlessly follow this tutorial, you should have a basic understanding of the following:

- JavaScript (We will be using TypeScript, but JavaScript knowledge is enough to follow this guide)
- Node.js
- Structure Query Language (SQL)
- How a server works

Let's begin!

## Setting Up A TypeScript Project with Express

The first steps to building a REST API with Express.js and TypeScript are:

1. Generating and configuring `package.json`
2. Installing basic dependencies necessary to initially run our app
3. Generating and configuring `tsconfig.json`

### 1. Generating and configuring `package.json` file
```bash
npm init -y
```
This command generates a `package.json` file in the root directory. 
Next, update the file to look like this:

```json
{
  "name": "article",
  "version": "1.0.0",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "dev": "node --loader=ts-node/esm --env-file=.env --watch src/index.ts",
    "build": "npx tsc",
    "start": "node --env-file=.env dist/index.js"
  },
  ...
}
```
The `main` option in the above `package.json` file points to the destination of the JavaScript file after compilation. The `dist/index.js` file will be generated (usually for production) when you execute the `build` script command by running `npm run build` on the terminal. The `start` command is used to start the server in production.

The `dev` script command is used to run the application in development mode. It contains the following commands:

- `node`: Uses the Node runtime to execute the code.
- `--loader=ts-node/esm`: Uses the `ts-node` package (we'll later install) to compile our code to JavaScript during development.
- `--env-file=.env`: Tells Node where the `.env` file is located.
- `-watch`: Reruns our code whenever we make an update, enabling automatic reloading during development.

### 2. Installing starter dependencies
We'll install some packages needed to kickstart our application. Aside from `typescript` and `express`, we will install the following packages:

- `ts-node`: A package that compiles our TypeScript code to JavaScript during development, allowing us to run our application without the need for explicit compilation.
- `@types/express`: A package that provides type definitions for Express.js, helping TypeScript to recognize and understand the types and interfaces of Express.js, enabling better code completion, error reporting, and overall development experience.

```bash
npm i express; npm i -D typescript ts-node @types/express
```
The `-D` flag, short for `--save-dev`, indicates that the packages being installed are development dependencies, meaning they are only required for development purposes and will not be needed in production. By using the `-D` flag, we are telling npm to include these packages in the `devDependencies` section of our `package.json` file, rather than the `dependencies` section, which is used for production dependencies.
### 3. Generating and configuring `tsconfig.json`
The following command generates the `tsconfig.json` file in the root directory:

```bash
npx tsc --init
```
The generated `tsconfig.json` file will contain many commented out TypeScript compiler options. Uncomment out the following options and update them accordingly:

```json
{
  "compilerOptions": {
    ....

    "module": "NodeNext",
    "rootDir": "./src",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    ....
  }
}
```
### Recommended starter file structure for the project
**Create the necessary folders and files**
Your file structure should look like this:

```
├── .env
├── .gitignore
├── package-lock.json
├── package.json
├── src
│   ├── db
│   ├── handlers  
│   ├── routes
│   ├── lib
│   ├── middleware
│   ├── index.ts
│   └── server.ts
└── tsconfig.js
```
## Express Middleware

We'll use middleware functions throughout the project. So, let's take some time to understand them. If you're already familiar with Express middleware, you can skip this section.

### Without middleware

To best illustrate this concept, I made a little sketch using [excalidraw](https://excalidraw.com/):

![Server without middleware](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w7j383lfd252qf0bmmfy.png)

Normally, this is how a client will communicate with a server. In this case, the client requests all the notes in the database. The server makes a request to the database and, depending on the database's response, returns the requested resources or an error.
This approach keeps the server open for all kinds of requests. As API developers, we want to ensure that the client is requesting the right way and is not a malicious actor. So, we introduce middleware into the equation:

### With middleware

![Server with middleware](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nen7h26xys08pjzbzur5.png)

The middleware sits between the client's request and the server's handler function. Whenever the server receives a request from the client, the request has to pass through the middleware and gets securitized in the process. In the above sketch, the middleware modified the request object. Express middleware can do more, including the following:

- Execute any code
- Terminate the request
- Make changes to the request or response object
- Detect errors
- Authentication and authorization

### How to define an express middleware

```typescript
import express, {Request, Response, NextFunction} from "express"
const app = express()

const logRequestMethod = (req: Request, res: Response, next: NextFunction) => {
	console.log(req.method)
	next()
}
const logHostname = (req: Request, res: Response, next: NextFunction) => {
	console.log(req.hostname)
	next()
}
```
`logRequestMethod` and `logHostname` are middlewares responsible for logging the request's HTTP method and hostname respectively. Typically, an Express middleware has three arguments:

- request
- response
- next

If we don't call the `next()` function, respond back to the client, or terminate the request, the server will hang in the middleware. The `next()` function transfers control to the next middleware or handler.

### How to use an express middleware

Basically, there are two ways of using an Express middleware:

- On an application level: An application-level middleware will execute whenever a request reaches the server and not terminated by other middleware before it. We can invoke application-level middleware using `app.use(middlewareFunction)`. For example, let's use the above `logRequestMethod` on the app level:

```typescript
app.use(logRequestMethod)
```
- Route-specific middleware: We can restrict a middleware to a particular route. This type of middleware will only execute when a request is made to that particular route. 
Note that whether we're using a middleware on the app or route level, we can have one or more of them separated by commas. For instance, let's use logRequestMethod and logHostname only when the client makes a request to the `/about` path:

```typescript
app.get("/about", logRequestMethod, logHostname, (req, res) => {})
```
The `app.use()` can also take a path as the first argument and used for a route-specific middleware: `app.use("/about", logRequestMethod)`.

### Error middleware

Whenever a client request results in an unhandled error, Express.js has a default error middleware that sends the error message back to the client in HTML format. There are two common ways an error can go unhandled:

- Not wrapping a code block that may result in an error in a try-catch
- The client sending a request to a route that doesn't exist

We would create the following middlewares and use them on the application level to handle errors:

- A custom error middleware that handles all errors and sends back an error response in JSON format to the client.
- A not-found middleware that gets triggered whenever the client requests resources from a route that doesn't exist. This not-found middleware will forward an error message to the custom error middleware, which will send back an appropriate JSON response to the client.

### Setting up the Error Middleware

As mentioned earlier, the `next()` function triggers a jump from the current middleware in execution to the next middleware or handler. However, if we pass an argument to the `next()` function, it jumps to the next error middleware on the stack. If we haven't defined a custom error middleware, it transfers control to Express's default error middleware.

Typically, you want to pass an Error object to the `next()` function. The issue with the inbuilt error class is that it only accepts one argument, which is the error message string. However, we want our error object to contain an error message and status code that we would use to send back the appropriate error message back to the client.

Let's create a child class of the error class and make it receive an extra `statusCode` argument. So, within the `lib` folder, create a `custom-error.ts` file and insert the following lines of code:

**`src/lib/custom-error.ts`**
```typescript
export class CustomError extends Error {
  message: string;
  statusCode: number;
  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
  }
}
```
Our `CustomError` class can form error objects with two arguments — the message and status code. We'll use `CustomError` throughout our code to forward errors that happen in our server to the error middleware we'll create in a moment.

Next, we'll create an error middleware that will receive the error object and form a proper JSON response for the client. Unlike the traditional middleware functions, error middleware takes four arguments — `error`, `request`, `response`, `next`.

Within the `middleware` folder, create `error.ts` file. Within, paste the following lines of code:

**`src/middleware/error.ts`**
```typescript
import { Request, Response, NextFunction } from "express";
import { CustomError } from "../lib/custom-error.ts";

export function error(
  err: CustomError,
  req: Request,
  res: Response,
  next: NextFunction
) {
  try {
    const msg = JSON.parse(err.message);
    res.status(err.status).json({ msg });
  } catch (error) {
    res.status(err.status).json({ msg: err.message });
  }
}
```
The `err` argument is the same error that is passed into the `next()` function from some middleware or handler. The error middleware can handle both JSON strings and text strings.

By handling both JSON and text strings, our error middleware can flexibly accommodate different types of error messages, making it more robust and adaptable to various scenarios.

### Setting up not-found middleware

We need to create a middleware that handles requests made to routes that do not exist. This is straightforward. Express executes all middleware in the stack except the request is terminated by some middleware. So, we'll simply create a middleware that forwards an error message to our error middleware and place it at the tail end of the middleware stack. At that position, it only gets hit when our Express app has scanned through all the routes we have defined and doesn't find any that matches the one the client is requesting.

Inside the middleware folder, create a `not-found.ts` file and paste the following lines of code:

**`src/middleware/not-found.ts`**
```typescript
import { Response, Request, NextFunction } from "express";
import { CustomError } from "../lib/custom-error.ts";

export function notFound(req: Request, res: Response, next: NextFunction) {
  return next(new CustomError("Route not found", 404));
}
```
This not-found middleware creates a `CustomError` object with a 404 status code and passes it to the next error middleware using the `next()` function.

### Setting up `server.ts` file

Let's set up the application `server.ts` file and implement the two middlewares we just created. Within the `src` folder, create the `server.ts` file:

**`src/server.ts`**
```typescript
import express, { urlencoded, json } from "express";
import { notFound } from "./middleware/not-found.ts";
import { error } from "./middleware/error.ts";

const app = express();
app.use(urlencoded({ extended: true }));
app.use(json());

// ... other middlewares and routes ...

app.use(notFound);
app.use(error);

export default app;
```
The order of middleware matters in Express, and it's important to place the error middleware last, as it will catch any errors that occur in the previous middlewares or handlers. The not-found middleware should be placed just above the error middleware, as it will handle any requests that don't match any of the previous routes.

The `urlencoded({ extended: true })` and `json()` middlewares are built-in Express middleware functions that parse requests with urlencoded and JSON payloads, respectively. They should be placed early in the middleware stack, as they need to parse the request body before any other middlewares or routes can handle the request.

## Setting up the Turso Database and Drizzle ORM

On this section, we’ll learn how to set up and use Turso database and Drizzle ORM in the API.

### How to set up a Turso database

To use Turso database, you need the connection URL and authentication token. Follow these steps to obtain the credentials:

1. Go to the [Turso website](https://turso.tech/)
2. Click on the `Sign Up` button
3. Sign up with either Gmail or Github
4. Choose a username
5. Skip the "About you" form
6. Click on `Create Database`
7. Name your database (e.g., *express-api-project*)
8. Click on `Create Database`
9. Scroll down and click on `Continue to Dashboard`
10. In the dashboard's side menu, click on `Databases`
11. Click on the name of the database you created (e.g., *express-api-project*) at the base of the page
12. Scroll down, copy the URL, and click on the `Generate Token` button
13. Leave everything as it is and click on `Generate Token`
14. Copy the token
15. Paste the URL and token in your `.env` file as follows:

```
TURSO_AUTH_TOKEN=paste-token-here
TURSO_CONNECTION_URL=paste-url-here
PORT=3000
```

### Installing necessary packages for our database

```bash
npm i @libsql/client drizzle-orm; npm i -D drizzle-kit
```
### Create the Drizzle configuration file

After installing the necessary packages, create a `drizzle.config.ts` file in the root directory and paste the following configuration options:

**`drizzle.config.ts`**
```typescript
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./src/db/migrations",
  driver: "turso",
  dbCredentials: {
    url: process.env.TURSO_CONNECTION_URL,
    authToken: process.env.TURSO_AUTH_TOKEN,
  },
  dialect: "sqlite",
  verbose: true,
  strict: true,
});
```
This file contains the configurations for drizzle ORM, including location of our schema and migrations. The `verbose: true` property prints out every action executed when making changes to the database. `strict:true` property ensures caution, forcing confirmation of any changes you want to make to the database.

### Connect Turso database with Drizzle

Next, create the `db.ts` file in the `db` folder and paste the following lines of code:

**`src/db/db.ts`**
```typescript
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
const client = createClient({
  url: process.env.TURSO_CONNECTION_URL,
  authToken: process.env.TURSO_AUTH_TOKEN,
});
export const db = drizzle(client);
```
The `drizzle(client)` function establishes a connection between the database client and Drizzle ORM, enabling the use of the ORM’s capabilities to interact with the database.

## Create Notes Schema and Migrations

Now that we have set up our database, let's create the notes schema, generate migrations, and apply the migrations to the Turso database.

### Creating the notes schema

Let's define our schema file for the notes app project. In the `db` folder, create a new file named `schema.ts` and add the following code:

**`src/db/schema.ts`**
```typescript
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";

export const NotesTable = sqliteTable("note", {
  id: integer("id").primaryKey(),
  title: text("title").notNull(),
  body: text("body"),
});
```
This is similar to the `CREATE TABLE` statement in MySQL. It creates a table with the following columns:

- `id`: A primary key that auto-increments, uniquely identifying each note
- `title`: A text column that must not be null, representing the title of the note
- `body`: A text column that can be null, representing the body or content of the note

### Generating migrations

Next, we'll generate migrations using the following command in your terminal:

```bash
npx drizzle-kit generate
```
This command will create our database migrations in the `"/src/db/migrations"` directory, as specified in the `drizzle.config.ts` file.

### Apply migrations

Next, apply the generated migrations to the Turso database by running the following command in your terminal:

```bash
npx drizzle-kit migrate
```
## Creating Notes Router in a Modular Way

When building an API using Express, we can place all our routes directly on the Express app:

```typescript
import express from "express";

const app = express();

//get request
app.get("/", handlerFunction1)

//post request
app.post("/add-something", handlerFunction2)
```
Using this approaching for small and demo applications is fine. But in large-scale projects, it can lead to a cluttered and hard-to-maintain codebase. Let’s see a better way.

### Express Router function explained

Express.js provides the `Router()` function, which creates a router object. This object acts as a middleware, but with the added capability to attach HTTP methods (such as GET, POST, PUT, and DELETE) to it. This allows for modular and organized routing.

Here's an example of creating a simple router:

```typescript
import { Router } from "express";

//defining the router
const routerObject = Router()

routerObject.get("/get-something", (req, res) => {})
routerObject.post("/post-something", (req, res) => {})
routerObject.delete("/delete-something", (req, res) => {})

//using the router in our app
app.use("/our-api", routerObject)
```
When a request URL starts with `/our-api`, the `routerObject` will be invoked, and the appropriate handler will be executed based on the HTTP method and the remaining part of the URL. For instance, if the request URL is `/our-api/get-something`, the GET handler attached to the `/get-something` route will be executed.

### Creating the `notesRouter` object

The notes API will have the following endpoints:

| API Endpoint | Method | Description |
| --- | --- | --- |
| /add-note | POST | Add new note |
| /get-note/:id | GET | Get note with id |
| /get-all-notes | GET | Get all notes |
| /update-note/:id | PUT | Update note with id |
| /delete-note/:id | DELETE | Delete note with id |

Within the `routes` folder, create `notes.ts` file and insert the following lines of code:

**`src/routes/notes.ts`**
```typescript
import { Router } from "express";
import { addNote, deleteNote, getAllNotes, getNote, updateNote} from "../handlers/notes.ts";

const notesRouter = Router();

notesRouter.get("/get-note/:id", getNote);
notesRouter.get("/get-all-notes", getAllNotes);
notesRouter.post("/add-note", addNote);
notesRouter.put("/update-note/:id", updateNote);
notesRouter.delete("/delete-note/:id", deleteNote);

export default notesRouter;
```
Now that our `notesRouter` object is ready, let’s hook it to our app in our `server.ts` file:

**`src/server.ts`**
```typescript
import express, { urlencoded, json } from "express";
import { notFound } from "./middleware/not-found.ts";
import { error } from "./middleware/error.ts";
import notesRouter from "./routes/notes.ts";

const app = express();
app.use(urlencoded({ extended: true }));
app.use(json());

app.use("/api", notesRouter);

app.use(notFound);
app.use(error);

export default app;
```
The URL to request any notes resource must start with `/api`. As we've seen, Express Router makes our application easier to manage and scale.

Next, we'll define our handlers, including `addNote` and `getNote`. Although we've imported them, we haven't created them yet.

## Creating the Notes Handlers

Handlers are technically middlewares. Just like middleware functions, they have three arguments — `request`, `response`, and `next`. Typically, they respond to the client or send an error message to the error middleware using the `next()` function.

More importantly, we define the logic to interact with the database in the handlers. This is where Drizzle shines. Within the `handlers` folder, create a new file named `notes.ts` and add the following code:

**`src/handlers/notes.ts`**
```typescript
import { eq } from "drizzle-orm";
import { db } from "../db/db.ts";
import { NotesTable } from "../db/schema.ts";
import { Response, Request, NextFunction } from "express";
import { CustomError } from "../lib/custom-error.ts";

export async function addNote(req: Request, res: Response, next: NextFunction) {
  try {
    const note = await db.insert(NotesTable).values(req.body).returning();
    res.status(201).json({ note });
  } catch (error) {
    next(new CustomError("Failed to add note", 500));
  }
}

export async function getAllNotes(req: Request,res: Response, next: NextFunction) {
  try {
    const notes = await db.select().from(NotesTable);
    res.status(200).json({ notes });
  } catch (error) {
    next(new CustomError("Failed to fetch notes", 500));
  }
}

export async function getNote(req: Request, res: Response, next: NextFunction) {
  try {
    const note = await db
      .select()
      .from(NotesTable)
      .where(eq(NotesTable.id, +req.params.id));
    res.status(200).json({ note });
  } catch (error) {
    next(new CustomError("Failed to fetch note", 500));
  }
}

export async function deleteNote(req: Request, res: Response, next: NextFunction) {
  try {
    const note = await db
      .delete(NotesTable)
      .where(eq(NotesTable.id, +req.params.id))
      .returning({
        deletedNoteId: NotesTable.id,
      });
    res.status(200).json({ note });
  } catch (error) {
    next(new CustomError("Failed to delete note", 500));
  }
}

export async function updateNote(req: Request, res: Response, next: NextFunction) {
  try {
    const note = await db
      .update(NotesTable)
      .set(req.body)
      .where(eq(NotesTable.id, +req.params.id))
      .returning();

    res.status(201).json({ note });
  } catch (error) {
    next(new CustomError("Failed to update note", 500));
  }
}
```
Similar to SQL, Drizzle provides methods like `insert`, `select`, `update`, `delete`, `set`, `where`, and more to interact with the database. The `eq` function is used to compare two entities. The `returning` function at the end of the `insert`, `update`, and `delete` functions specifies what Drizzle should return after successful execution. If left empty, it will return all fields. Drizzle offers a wide range of functions to help you achieve your API goals, and they are well-documented in the [official documentation](https://orm.drizzle.team/docs/overview).

## Validating and Sanitizing Data from Client’s Request

Our app is still missing a crucial component. Consider a scenario where we expect a numerical value in the request body but receive a string instead, or anticipate an email address but receive plain text. Even worse, a malicious actor might attempt to inject code into our database to steal or compromise stored information. We must prevent these scenarios from occurring, and one way to do so is to thoroughly examine client data before it reaches the handler.

We could write custom middlewares to validate and sanitize client data, but a more efficient approach is to utilize the battle-tested [express-validator](https://express-validator.github.io/docs) npm package. This package provides a set of middlewares that enable us to inspect client data and ensure it meets our requirements, making our app more secure and robust.

### Installing `express-validator`

```bash
npm i express-validator
```
### Defining `express-validator` middlewares

Within the `src/lib` folder, create the `validator-functions.ts` file and include the following lines of code:

**`src/lib/validator-functions.ts`**
```typescript
import { body, param } from "express-validator";

//Validating and sanitizing title from request body
export function validateNoteTitle() {
  return body("title").notEmpty().isString().trim().escape();
}

//Validating and sanitizing body from request body
export function validateNoteBody() {
  return body("body").notEmpty().isString().trim().escape();
}

//Validating id from route parameter
export function validateIdParam() {
  return param("id").toInt().isInt();
}
```
To validate individual values from the client request, we utilize method chaining, where the output of the previous method becomes the input for the current method. For the `title` and `body` fields, we employ the following methods:

- `notEmpty()`: Validates that the value is not empty
- `isString()`: Validates that the value is a string
- `trim()`: A sanitizer that removes whitespace from both ends of the value, if present
- `escape()`: A sanitizer that replaces special characters, such as `<` and `>`, with HTML entities, protecting the server from Cross-Site Scripting (XSS) attacks

For a comprehensive list of validators and sanitizers, along with their uses, refer to the [validator.js](https://github.com/validatorjs/validator.js) GitHub repository.

### Implementing `express-validator` middleware functions

Now that we have written the validator functions, we can now implement them in our routes. Let’s update our `src/routes/notes.ts` file:

**`src/routes/notes.ts`**
```typescript
import { Router } from "express";
import { addNote, deleteNote, getAllNotes, getNote, updateNote} from "../handlers/notes.ts";
import { validateIdParam, validateNoteBody, validateNoteTitle} from "../lib/validator-functions.ts";

const notesRouter = Router();

notesRouter.get("/get-note/:id", validateIdParam(), getNote);
notesRouter.get("/get-all-notes", getAllNotes);
notesRouter.post("/add-note", validateNoteBody(), validateNoteTitle(), addNote);
notesRouter.put("/update-note/:id", validateIdParam(), validateNoteBody(), validateNoteTitle(), updateNote);
notesRouter.delete("/delete-note/:id", validateIdParam(), deleteNote);

export default notesRouter;
```
When an `express-validator` middleware encounters a validation error, it doesn't terminate the request immediately. Instead, it collects the errors and passes control to the next middleware or handler in the chain. This allows for more flexibility in handling validation errors.

The `validationResult()` function is a key part of this process. It takes the request object as an argument and returns an object containing the validation errors, if any. By checking the result of `validationResult()`, we can determine if there were any validation errors and handle them accordingly in the handlers.

### Capturing validation errors

Let’s update our `src/handlers/notes.ts` to account for validation errors:

**`src/handlers/notes.ts`**
```typescript
import { eq } from "drizzle-orm";
import { db } from "../db/db.ts";
import { NotesTable } from "../db/schema.ts";
import { Response, Request, NextFunction } from "express";
import { CustomError } from "../lib/custom-error.ts";
import { validationResult } from "express-validator";

export async function addNote(req: Request, res: Response, next: NextFunction) {
  const result = validationResult(req);
  console.log(result);
  if (!result.isEmpty()) {
    return next(new CustomError(JSON.stringify(result.array()), 400));
  }
  try {
    const note = await db.insert(NotesTable).values(req.body).returning();
    res.status(201).json({ note });
  } catch (error) {
    next(new CustomError("Failed to add note", 500));
  }
}

export async function getAllNotes(req: Request, res: Response, next: NextFunction) {
  try {
    const notes = await db.select().from(NotesTable);
    res.status(200).json({ notes });
  } catch (error) {
    next(new CustomError("Failed to fetch notes", 500));
  }
}

export async function getNote(req: Request, res: Response, next: NextFunction) {
  const result = validationResult(req);
  if (!result.isEmpty()) {
    return next(new CustomError(JSON.stringify(result.array()), 400));
  }
  try {
    const note = await db
      .select()
      .from(NotesTable)
      .where(eq(NotesTable.id, +req.params.id));
    res.status(200).json({ note });
  } catch (error) {
    next(new CustomError("Failed to fetch note", 500));
  }
}

export async function deleteNote(req: Request, res: Response, next: NextFunction) {
  const result = validationResult(req);
  if (!result.isEmpty()) {
    return next(new CustomError(JSON.stringify(result.array()), 400));
  }
  try {
    const note = await db
      .delete(NotesTable)
      .where(eq(NotesTable.id, +req.params.id))
      .returning({
        deletedNoteId: NotesTable.id,
      });
    res.status(200).json({ note });
  } catch (error) {
    next(new CustomError("Failed to delete note", 500));
  }
}

export async function updateNote(req: Request, res: Response,next: NextFunction) {
  const result = validationResult(req);
  if (!result.isEmpty()) {
    return next(new CustomError(JSON.stringify(result.array()), 400));
  }
  try {
    const note = await db
      .update(NotesTable)
      .set(req.body)
      .where(eq(NotesTable.id, +req.params.id))
      .returning();

    res.status(201).json({ note });
  } catch (error) {
    next(new CustomError("Failed to update note", 500));
  }
}
```
In this updated code, we're using `validationResult(req)` to check for validation errors. If there are any errors, we're returning a 400 response with the error details. If there are no errors, we can proceed with the logic.

`result.array()` returns an array of objects, and the `CustomError` class expects a string message. By using `JSON.stringify()`, we can convert the error message to a string, which can then be passed to the `CustomError` class. Remember that we designed our error middleware to accept both JSON and text strings? This is where it comes in handy in this API. 

## Testing the app

To test the app, insert the following lines of code in the `index.ts` file:

**`src/index.ts`**
```typescript
import app from "./server.ts";
const port = process.env.PORT || 8000;

app.listen(port, () => {
  console.log(`Server is listening at port ${port}`);
});
```
Run the app with the following command on the terminal:

```bash
npm run dev
```
I am using Thunder Client VS Code extension (which you can easily install) to test the API endpoints. However, you can any API client of your choice, such as Insomnia and Postman.

### Create note with the `/api/add-note` POST endpoint

![Post note request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/csj7blnqxzpgh481n74c.png)
Great, let's verify that our notes are being stored in the database! Here are the steps to follow:

1. Open your Turbo dashboard
2. Click on `Databases`
3. Select the database named _express-api-project_ (or the name you chose for your database)
4. Scroll down and click on the `Edit Tables` button next to the `Generate Token` button
5. In the `Tables` tab, click on the `notes` table
6. You should see a list of notes that you've created using the API

If you see the following screen, that means your server is working correctly and storing data in the database!

![Database with notes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2a1o7qdec8aaree52dsw.png)

### Fetching all notes with the /api/get-all-notes GET endpoint

![Request to get all notes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/durxlvrtzvwpyyduv656.png)
These endpoints worked! You can try the other endpoints yourself.
 
## Conclusion

Great job if you stuck around throughout the guide! You now have a solid foundation on how to build an API using TypeScript, Express, Drizzle, and Turso database. For reference, check out the project's [Github Repository](https://github.com/woodmark-dev/express-typescript-api). I hope you found this article helpful and enjoyed building this project as much as I did.