Build an Express server in TypeScript, from scratch

Learn how to build a server from scratch, using Node.js, Express, and TypeScript.

May 01, 2023


👈 Get the Code on GitHub
👈 Watch the Video on YouTube

Introduction

If you're like many JavaScript developers, you want to write in TypeScript. It sounds great, but how do you use it in the real world? In this tutorial, we'll build a TypeScript project from scratch, using Node.js, Express, and TypeScript.

Prerequisites

You should be able to follow this tutorial if you have a basic understanding of Node.js and TypeScript. If you're new to TypeScript, get my free guide: TypeScript Essentials: 5 Critical Keys to Using TypeScript Effectively

Outline

Here's an overview of the steps we'll take in this tutorial:

Install TypeScript

To install TypeScript, you can use npm, the Node.js package manager, by following these steps:

Open your terminal or command prompt.

Run the following command to install TypeScript globally:

npm install -g typescript

This will install the latest stable version of TypeScript on your machine.

To check if TypeScript is installed correctly, you can run the following command in your terminal:

tsc --version

This should print the version of TypeScript installed on your machine.

Now you're ready to use TypeScript in your Node.js project.

Clone the Project

For this tutorial, we'll be using the project, Pantry Pal, as a starting point. I recommend to fork it and clone it instead of cloning it directly. Note: the project already has the npm project initialized, so you can skip that step.

⚠️ There is a solution branch in the project that you can use to compare your code to the finished project.

Install Dependencies

We'll need to install the required dependencies for TypeScript. Run this in your terminal, at the root of your project:

npm install typescript ts-node @types/node --save-dev

Create a tsconfig file

Create a tsconfig.json file to configure TypeScript. You can do this manually or by running the following command in your terminal.

tsc --init

This generates a file with a few configs plus a lot of comments. Feel free to edit the comments until you have just the configs:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Create a new TypeScript server file

We'll create a new TypeScript server file in our project directory.

touch app.ts

Add the Start Scripts

Now we'll update our package.json file to include a script to run our TypeScript code. Add the following under the scripts property in your package.json file.

  "start": "ts-node app.ts"

This is the command we'll use when developing our project. It will run our TypeScript code using ts-node.

Also, we'll add another script to build a production version of our project. Add the following under the scripts property in your package.json file.

  "build": "tsc"

Write the Server Code

Finally, the fun part! We get to write the code for our server.

First, let's install Express

npm install express

Next, open the app.ts file and add the first line to import express.

import express from 'express';

Install Type Definitions

When we write just this first line, TypeScript is already mad at us! It gives us an error like this:

Cannot find module 'express' or its corresponding type declarations.

This is because we haven't installed the type definitions for Express. We can fix this by installing the @types/express package.

npm install @types/express

Now, TypeScript is happy, yay! But, what exactly happened, here? The @types/express package is a type definition file. It's a file that tells TypeScript what types to expect from the express package. It's not a package that we need to import into our code. It's just a file that TypeScript uses to help it understand the types of the express package. And now we can write our server code.

// Import express
import express from 'express';

// Create a new express app instance
const app = express();

// Make the PORT configurable
const { PORT = 3000 } = process.env;

// Start the Express server, displaying the localhost URL
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

This is great, but there aren't any routes for our server 😱. Let's add a route to serve up some recipes.

// ... express initialization above

// GET /recipes
app.get('/recipes', (req, res) => {
  res.send('Recipes to come');
});

Now we should see these words when we request the route on our server at http://localhost:3000/recipes

Watch the Code

You'll notice that when making changes, nothing happens. The server isn't restarting, nor is typescript recompiling our code. We can fix this by updating our start script in our package.json file. Before we do so, though, let's move our app.ts and recipes.json files into a src folder. This will help keep our project organized.

mkdir src
mv app.ts src
mv recipes.json src

Now our project should look like this

├── README.md
├── package-lock.json
├── package.json
├── src
│   ├── app.ts
│   └── recipes.json
└── tsconfig.json

Now, we can update our start script:

    "start": "nodemon --watch 'src/**' --ext 'ts,json' --ignore 'src/**/*.spec.ts' --exec 'ts-node src/app.ts'",

This script will watch our src folder for changes to any .ts or .json files. It will ignore any .spec.ts files. It will run ts-node on our app.ts file. However, we'll now need to install nodemon to watch our files.

npm install nodemon

Finally, we can run our start script to watch our files and run our server.

npm start

Now we should see the response from our server when we request the root of our server at http://localhost:3000/recipes.

Send Back Recipes

Instead of boring boilerplate code, let's add some recipes to our server. We'll use the pre-existing recipes.json file in the root of our project.

// Import the recipes
import recipes from './recipes.json';

// ... express initialization and other stuff

// GET /recipes
app.get('/recipes', (req, res) => {
  res.send(recipes);
});

When we do this, we get an error:

Cannot find module './recipes.json'. Consider using '--resolveJsonModule' to import module with '.json' extension.

Typescript basically is saying "I don't know what to do with this file. It's not a JavaScript file, and it's not a TypeScript file. What do you want from me?" We can fix this by adding the resolveJsonModule flag to our tsconfig.json file. Then, TypeScript will know how to handle JSON files.

  "resolveJsonModule": true

Now, our import works, and we are sending back recipes. Visit the API, and you'll see the recipes, in all their glory!

Great! We've created a simple API using TypeScript and Express. But... you might be wondering why TypeScript looks so much like JavaScript. What's the point? Well, let's add a new route to our server, and see how TypeScript can help us.

Add a DB module

In the next step, we'll add a new route, but first, let's create a lightweight in-memory "database".

touch src/db.ts

In this file, we'll create a mock db that basically mimics some functions that we might use to interact with a real database. We'll export a few functions that we can use to add and get recipes.

import recipes from './recipes.json';

export const get = () => {
  return recipes;
}

export const getByIndex = (idx) => {
  return recipes[idx];
}

You'll notice that the new function throws an error:

Parameter 'idx' implicitly has an 'any' type.

This is because TypeScript doesn't know what type idx is. We can fix this by adding a type annotation to the function parameter. What type do we expect? A number, to access the object in the array via index. So, we can add a type annotation to the parameter. Also, let's add a return type annotation to the function.

export const getByIndex = (idx: number) => {
  return recipes[idx];
}

Add a new route by id

Now, we can use this function in our server code.

// Import the db module
import * as db from './db';

// ... express initialization and other stuff

// GET /recipes/:idx
app.get('/recipes/:idx', (req, res) => {
  const idx = req.params.idx;
  const recipe = db.getByIndex(idx);
  res.send(recipe);
});

But, oops! Now we get an error in our route handler:

Argument of type 'string' is not assignable to parameter of type 'number'.

This is because req.params.idx is a string, but we're passing it to a function that expects a number. We can fix this by casting the string to a number.

const idx = Number(req.params.idx);

Now, before we start cursing TypeScript for all these errors, lets take a step back and think about what's going on. We're using TypeScript to help us catch errors before we run our code. This is a good thing! It's much easier to catch errors in our code before we run it, than to find them after we've deployed our code to production. So, rejoice in TypeScript, and fix your errors! 😄

Now, we can request a recipe by index at http://localhost:3000/recipes/1.

Add a Recipe

Let's add a new route to add a recipe to our database. We'll use the POST method to add a new recipe.

// POST /recipes
app.post('/recipes', (req, res) => {
  const recipe = req.body;
  db.add(recipe);
  res.send(recipe);
});

Of course, we'll also need an add function in our db module.

export const add = (recipe) => {
  recipes.push(recipe);
}

But we run into the same error here:

Parameter 'recipe' implicitly has an 'any' type.

Ok, we know that we need to give it a type, but what type do we give for an object like the recipes? This is where we'll need to create an interface!

Create an interface

An interface is a TypeScript construct that allows us to define the shape of an object. We can use interfaces to define the shape of our recipes, and then use them in our code. In this case, we want to define the shape of a recipe, so we'll call it Recipe.

export interface Recipe {
  name: string;
  description: string;
  ingredients: string[];
}

Now, we can use this interface in our db module. This way, when we call add, TypeScript knows what we'll be passing in.

export const add = (recipe: Recipe) => {
  recipes.push(recipe);
}

Now, we can use our db.add function in our route handler. Don't forget to configure Express to parse JSON.

// Configure Express to parse JSON
app.use(express.json());

// POST /recipes
app.post('/recipes', (req, res) => {
  const recipe = req.body;
  db.add(recipe);
  res.send(recipe);
});

Now we can add recipes to our server. We can test this out by using curl to send a POST request to our server.

curl -X POST -H "Content-Type: application/json" -d '{"name": "Pancakes", "description": "Delicious pancakes", "ingredients": ["flour", "eggs", "milk"]}' http://localhost:3000/recipes

And to confirm, we can visit http://localhost:3000/recipes/5 to see our new recipe!

And that's it! We've created an API using TypeScript and Express. We've also learned a bit about TypeScript, and how it can help us catch errors before we run our code. We've also learned about interfaces, which allow us to define the shape of an object. We can use interfaces to define the shape of our data, and then use them in our code.


Want to boost your TypeScript knowledge, fast?

👉 Get the FREE guide. 👈


TypeScript Essentials: 5 Critical Keys to Using TypeScript Effectively

Profile picture

Written by Preston Wallace I empower new and seasoned engineers to level-up their skills, get that promotion, and ultimately crush it in life. 👉 Follow me on LinkedIn!