Building a Simple API on Deno with Express
Tags: deno, typescript, tutorial, express
Published: 2020-05-28 13:14:05

[Deno](https://deno.land) is a new secure runtime for JavaScript and TypeScript created by Ryan Dahl, the original founder of Node.js.

Unlike Node, Deno does not use NPM or node modules for it's package management. In order to use third party modules you have to instead use browser compatible URLs which resolve to valid ES Modules - commonjs is not supported!

Deno also has a completely different underlying [core](https://doc.deno.land/https/github.com/denoland/deno/releases/latest/download/lib.deno.d.ts) and [standard](https://deno.land/std) library to Node meaning that a lot of libraries are no longer compatible because everything from file-system operations to HTTP requests have a new API.

> Fortunately, several module authors have already started converting Node modules over to support TypeScript and Deno. If you are a module maintainer for a Node module and are looking to support Deno, I recommend you check out the [Denoify](https://github.com/garronej/denoify) by [@GarroneJoseph](https://dev.to/garronej).

For this tutorial we are going to the [Opine](https://github.com/asos-craigmorten/opine) web framework - a fast, minimalist web framework for Deno ported from Express. It has almost exactly the same API as Express, and is a direct port to Deno, so the internal mechanics match Express exactly.

We will be building a simple Cat API for querying cat data! :cat:

# Installing Deno

Deno can be installed using all the main package installers as well using the official installer scripts. Here are some of the main ways to install:

*Shell (Mac, Linux):*

```bash
curl -fsSL https://deno.land/x/install/install.sh | sh
```

*PowerShell (Windows):*

```pwsh
iwr https://deno.land/x/install/install.ps1 -useb | iex
```

*Homebrew (Mac):*

```console
brew install deno
```

*Chocolatey (Windows):*

```console
choco install deno
```

Head over to the [Deno installation page](https://deno.land/#installation) for other installation methods and further details.

# Getting started

Having installed Deno you can now run make use of the `deno` command. Use `deno help` to explore the commands on offer. We'll be using this command to run our API server later on.

Let's go and create our project! In a new directory create the following files:

```
.
├── deps.ts
├── db.ts
└── server.ts
```

`server.ts` will contain our main server code, `db.ts` will hold our mock database code and `deps.ts` will hold all of our dependencies and versions (a bit like a substitute `package.json`).

## Importing our dependencies

In the `deps.ts` file we add the following to re-export our required dependencies at the versions we need:

```ts
export { opine } from "https://deno.land/x/opine@0.21.2/mod.ts";
```

Notice that we're importing it from a url? That's right, in Deno you can import modules from any URL and relative or absolute file path that exports a valid ES Module.

This means you can easily pull in any code from the web, e.g. gists, GitHub code and are no longer tied to versions that have been released - if there's something on a `main` branch (or any other feature branch!) that you can't wait to try, you can just import it!

We could choose to not use a `deps.ts`, and import these dependencies directly into our server code, but using a `deps.ts` file is useful for when you want to upgrade a dependency as you can change the version in one place rather than in all your files (especially in large projects)!

## Writing our mock database

In `db.ts` we create a simple array of objects to represent our dummy database:

```ts
export const database = [
  {
    id: "abys",
    name: "Abyssinian",
    url: "http://cfa.org/Breeds/BreedsAB/Abyssinian.aspx",
    description:
      "The Abyssinian is easy to care for, and a joy to have in your home. They’re affectionate cats and love both people and other animals.",
  },
  {
    id: "aege",
    name: "Aegean",
    url: "http://www.vetstreet.com/cats/aegean-cat",
    description:
      "Native to the Greek islands known as the Cyclades in the Aegean Sea, these are natural cats, meaning they developed without humans getting involved in their breeding. As a breed, Aegean Cats are rare, although they are numerous on their home islands. They are generally friendly toward people and can be excellent cats for families with children.",
  },
  {
    id: "abob",
    name: "American Bobtail",
    url: "http://cfa.org/Breeds/BreedsAB/AmericanBobtail.aspx",
    description:
      "American Bobtails are loving and incredibly intelligent cats possessing a distinctive wild appearance. They are extremely interactive cats that bond with their human family with great devotion.",
  },
  {
    id: "acur",
    name: "American Curl",
    url: "http://cfa.org/Breeds/BreedsAB/AmericanCurl.aspx",
    description:
      "Distinguished by truly unique ears that curl back in a graceful arc, offering an alert, perky, happily surprised expression, they cause people to break out into a big smile when viewing their first Curl. Curls are very people-oriented, faithful, affectionate soulmates, adjusting remarkably fast to other pets, children, and new situations.",
  },
  {
    id: "asho",
    name: "American Shorthair",
    url: "http://cfa.org/Breeds/BreedsAB/AmericanShorthair.aspx",
    description:
      "The American Shorthair is known for its longevity, robust health, good looks, sweet personality, and amiability with children, dogs, and other pets.",
  },
  {
    id: "awir",
    name: "American Wirehair",
    url: "http://cfa.org/Breeds/BreedsAB/AmericanWirehair.aspx",
    description:
      "The American Wirehair tends to be a calm and tolerant cat who takes life as it comes. His favorite hobby is bird-watching from a sunny windowsill, and his hunting ability will stand you in good stead if insects enter the house.",
  },
];

```

This data has been taken from [TheCatAPI - Cats as a Service, Everyday is Caturday](https://thecatapi.com/), a free to use public API.

## Server setup and our first endpoint

Now let's get started on writing our server!

```ts
import { opine, Router } from "./deps.ts";
import { database } from "./db.ts";

const app = opine();
const v1ApiRouter = Router();

// Add our /cats route to the v1 API router
// for retrieving a list of all the cats.
v1ApiRouter.get("/cats", (req, res) => {
  res.setStatus(200).json({
    success: "true",
    data: database,
  });
});

// Mount the v1 API router onto our server
// at the /api/v1 path.
app.use("/api/v1", v1ApiRouter);

const PORT = 3000;

// Start our server on the desired port.
app.listen(PORT);

console.log(`API server running on port ${PORT}`);
```

First we import `opine` and `Router` from the Opine module in our `deps.ts` and we create a new Opine `app` and a `v1ApiRouter` router which is going to be used to define endpoints on the v1 of our API.

We then use the `get()` method on the `v1ApiRouter` to define a route to handle GET requests to endpoints matching the `/cats` path using the first parameter. The second parameter is a function that runs every time we hit that endpoint. This function takes two parameters which are `req` and `res` (though you can name these arguments however you like). The `req` object contains information about our request and the `res` object contains properties and methods for manipulating what information we send back to the user that requested the endpoint.

```ts
res.setStatus(200).json({
  success: "true",
  data: database,
});
```

Using the `res.setStatus()` method we set the HTTP status code to `200` (OK) to let the user know that the request was successful. We then use the `res.json()` method, chained off of the `res.setStatus()` method, to send a JSON object back to the user as the response, containing our cat database information. You don't have to chain these methods and could equally write something like:

```ts
res.setStatus(200)
res.json({
  success: "true",
  data: database,
});
```

We then add our v1 API router into our Opine app on the `/api/v1` path by using the `app.use()` method:

```ts
app.use("/api/v1", v1ApiRouter);
```

This command will now route any requests whose URL starts with `/api/v1` to our v1 API Router.

Finally, we define a `PORT` as a constant and execute the `app.listen()` command to start the server.

If you are familiar with Express you will notice that these commands are almost exactly the same as what you would use when writing an Express application for Node. For comparison, here's the same code in Node:

```js
const express = require("express");
const { database } = require("./database");

const app = express();
const v1ApiRouter = express.Router();

v1ApiRouter.get("/cats", (req, res) => {
  res.setStatus(200).json({
    success: "true",
    data: database,
  });
});

app.use("/api/v1", v1ApiRouter);

const PORT = 3000;

app.listen(PORT, () => console.log(`API server running on port ${PORT}`));
```

And that's it! Let's run our API server and see what happens :smile:.

Run the following to start the server and then head to <http://localhost:3000/api/v1/cats> to see what it responds with:

```bash
deno run --allow-net ./server.ts
```

You should see something in your browser like the response below.

![Prettified cat API JSON response object in a browser](https://dev-to-uploads.s3.amazonaws.com/i/n38iq4dszcj44wu1lz15.png)

You successfully just written your first API in Deno! :tada:

## Upload a cat API endpoint

We now have a way to get our cat details from our API, but we have no way to upload more cats :cat:. Let's write an upload endpoint now!

Make the following changes to your `server.ts`:

```ts
import { opine, Router } from "./deps.ts";
// *** NEW ***
import { getDatabase, addToDatabase } from "./db.ts"; 

const app = opine();
const v1ApiRouter = Router();

// Add our /cats route to the v1 API router
// for retrieving a list of all the cats.
v1ApiRouter.get("/cats", (req, res) => {
  res.setStatus(200).json({
    success: "true",
    data: getDatabase(), // *** NEW ***
  });
});

// *** NEW ***
// Add our /cats route to the v1 API router
// for uploading a cat to the database.
v1ApiRouter.put("/cats", (req, res) => {
  const cat = req.parsedBody;
  addToDatabase(cat);
  res.sendStatus(201);
});

// *** NEW ***
// We use the Opine JSON body parser to allow
// us to parse the upload cat JSON object.
app.use(json());

// ... the remaining code from our previous example
```

There's a few changes here:

1. We now import `getDatabase` and `addToDatabase` methods from `db.ts` for getting the database data, and for adding to the database. We will see how we write these methods a bit later.
2. For the GET `/cats` request handler, we update `data` property in the JSON response to come from the method `getDatabase()`.
3. We have then added a new route handler onto the `v1ApiRouter` to handle PUT requests to the `/cats` endpoint. The function parameter takes the special property `req.parsedBody` which will contain our JSON cat object that we will be uploading and stores it in the variable `cat`. This new cat object is then added to the database using the `addToDatabase()` method. Finally the function calls the `res.sendStatus()` method with HTTP status code `201` (Created) to let the user know that they have successfully added the new cat to the database.
4. The last change is the addition of `app.use(json())`. This is adding a special function from the Opine module which returns a middleware that will take the `req.body` of every request, and if it is a JSON request, will parse the JSON and store it on the `req.parsedBody` property. This is what allows use to access the cat data in our new PUT endpoint.

Now we just need to add the new database methods to our `db.ts`:

```ts
// ... the previous code from our example

export const getDatabase = () => database;

export const addToDatabase = (
  cat: { id: string; name: string; url: string; description: string },
) => database.push(cat);
```

Here we export a `getDatabase()` method that just returns the `database` array, and also export a `addToDatabase()` method that accepts a cat object and performs a `database.push(cat)` to add the cat object to the `database` array.

Let's run the server again and see if we can upload a cat!

```bash
deno run --allow-net ./server.ts
```

Below is a code snippet for using the terminal command `curl` to make a PUT request to the new `/api/v1/cats` endpoint, but you can also use any request making platform such as [Postman](https://www.postman.com/).

```bash
curl -X PUT http://localhost:3000/api/v1/cats \
  -d '{ "id": "top-cat", "name": "Top Cat", "url": "https://en.wikipedia.org/wiki/Top_Cat", "description": "Top Cat (or simply T.C.) is the yellow-furred, charismatic, and clever cat." }' \
  -H 'Content-Type: application/json'
```

Here we are making a PUT request to our endpoint and passing a JSON object as the data containing information about our cat [Top Cat](https://en.wikipedia.org/wiki/Top_Cat). We are also careful to add the `Content-Type` header to the request so that the Opine server knows that the request body is JSON.

Execute the command and then open <http://localhost:3000/api/v1/cats> in the browser again...

![Updated cat API JSON response object containing the Top Cat object in a browser](https://dev-to-uploads.s3.amazonaws.com/i/0rbdlc2oiqd7aann1ggg.png)

Voila! :tada: :tada: We can see that our new cat has been added to the database :smile:

## Next steps

We've just seen how to implement a GET and PUT endpoint for retrieving and uploading cats to a database in an Opine server using Deno. We've seen how we can mount a router onto an applications, use multiple route handlers and also how to add a JSON parsing middleware for processing JSON request bodies.

If you want to take this further, why not try implementing one of the following:

- Add a GET `/cat/:id` endpoint for getting just a single cat by using it's id. You can use [this Opine example](https://github.com/asos-craigmorten/opine/tree/main/examples/downloads) for some guidance on how you might implement a wildcarded route, or you can check out the [Opine Router Docs](https://github.com/asos-craigmorten/opine/blob/main/.github/API/router.md#routermethodpath-callback--callback) for more help.
- Add a DELETE `/cat/:id` endpoint for deleting a cat by id. This should be very similar to the one above, the tricky bit will be making sure you delete the correct cat from the database!
- Add some validation to your endpoints. What if the user tries to get or delete a cat that doesn't exist? What if the user tries to upload a cat that is missing properties? Why not add some `if` statements to make sure that the request is valid, and if not, return a `400` (Bad Request) or `404` (Not Found).

That's all gang! Would love to hear your thoughts and how you're getting on with Deno - drop your comments and questions below! Will happily try to help :smile: