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:
- Generating and configuring
package.json
- Installing basic dependencies necessary to initially run our app
- Generating and configuring
tsconfig.json
1. Generating and configuring package.json
file
npm init -y
This command generates a package.json
file in the root directory.
Next, update the file to look like this:
{
"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 thets-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.
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:
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:
{
"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:
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
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
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 abovelogRequestMethod
on the app level:
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:
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
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
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
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
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:
- Go to the Turso website
- Click on the
Sign Up
button - Sign up with either Gmail or Github
- Choose a username
- Skip the "About you" form
- Click on
Create Database
- Name your database (e.g., express-api-project)
- Click on
Create Database
- Scroll down and click on
Continue to Dashboard
- In the dashboard's side menu, click on
Databases
- Click on the name of the database you created (e.g., express-api-project) at the base of the page
- Scroll down, copy the URL, and click on the
Generate Token
button - Leave everything as it is and click on
Generate Token
- Copy the token
- 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
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
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
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
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:
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:
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:
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:
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
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
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
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.
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 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
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
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 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
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
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
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:
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
Great, let's verify that our notes are being stored in the database! Here are the steps to follow:
- Open your Turbo dashboard
- Click on
Databases
- Select the database named express-api-project (or the name you chose for your database)
- Scroll down and click on the
Edit Tables
button next to theGenerate Token
button - In the
Tables
tab, click on thenotes
table - 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!
Fetching all notes with the /api/get-all-notes GET endpoint
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. I hope you found this article helpful and enjoyed building this project as much as I did.
Top comments (2)
Just enough what i wanted to start my project. Big thanks!
Informative! Thank you!