Building a Simple API on Deno with Express

Deno 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 and standard 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 by @GarroneJoseph.

For this tutorial we are going to the 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! 🐱

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):

curl 

-fsSL

https://deno.land/x/install/install.sh | sh

Enter fullscreen mode

Exit fullscreen mode

PowerShell (Windows):

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

Enter fullscreen mode

Exit fullscreen mode

Homebrew (Mac):

brew install deno

Enter fullscreen mode

Exit fullscreen mode

Chocolatey (Windows):

choco install deno

Enter fullscreen mode

Exit fullscreen mode

Head over to the Deno installation page 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

Enter fullscreen mode

Exit fullscreen mode

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:

export

{

opine

}

from

"

https://deno.land/x/[email protected]/mod.ts

"

;

Enter fullscreen mode

Exit fullscreen mode

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:

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

:

"

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.

"

,

},

];

Enter fullscreen mode

Exit fullscreen mode

This data has been taken from TheCatAPI – Cats as a Service, Everyday is Caturday, a free to use public API.

Server setup and our first endpoint

Now let’s get started on writing our server!

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

}

`

);

Enter fullscreen mode

Exit fullscreen mode

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.

res

.

setStatus

(

200

).

json

({

success

:

"

true

"

,

data

:

database

,

});

Enter fullscreen mode

Exit fullscreen mode

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:

res

.

setStatus

(

200

)

res

.

json

({

success

:

"

true

"

,

data

:

database

,

});

Enter fullscreen mode

Exit fullscreen mode

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

app

.

use

(

"

/api/v1

"

,

v1ApiRouter

);

Enter fullscreen mode

Exit fullscreen mode

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:

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

}

`

));

Enter fullscreen mode

Exit fullscreen mode

And that’s it! Let’s run our API server and see what happens 😄.

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

deno run 

--allow-net

./server.ts

Enter fullscreen mode

Exit fullscreen mode

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

Prettified cat API JSON response object in a browser

You successfully just written your first API in Deno! 🎉

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 🐱. Let’s write an upload endpoint now!

Make the following changes to your server.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

Enter fullscreen mode

Exit fullscreen mode

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:

// ... 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

);

Enter fullscreen mode

Exit fullscreen mode

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!

deno run 

--allow-net

./server.ts

Enter fullscreen mode

Exit fullscreen mode

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.

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'

Enter fullscreen mode

Exit fullscreen mode

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. 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

Voila! 🎉 🎉 We can see that our new cat has been added to the database 😄

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 for some guidance on how you might implement a wildcarded route, or you can check out the Opine Router Docs 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 😄