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! 🐱
Nội Dung Chính
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.
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:
- We now import
getDatabase
andaddToDatabase
methods fromdb.ts
for getting the database data, and for adding to the database. We will see how we write these methods a bit later. - For the GET
/cats
request handler, we updatedata
property in the JSON response to come from the methodgetDatabase()
. - 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 propertyreq.parsedBody
which will contain our JSON cat object that we will be uploading and stores it in the variablecat
. This new cat object is then added to the database using theaddToDatabase()
method. Finally the function calls theres.sendStatus()
method with HTTP status code201
(Created) to let the user know that they have successfully added the new cat to the database. - 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 thereq.body
of every request, and if it is a JSON request, will parse the JSON and store it on thereq.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…
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 a400
(Bad Request) or404
(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 😄