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
PowerShell (Windows):
iwr https://deno.land/x/install/install.ps1 -useb | iex
Homebrew (Mac):
brew install deno
Chocolatey (Windows):
choco install deno
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
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/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:
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, 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}`);
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,
});
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,
});
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);
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}`));
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
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
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);
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
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'
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 😄
Top comments (5)
Great article. Unfortunately, this does not work any more. We should use the latest version of opine and include, json and Router in the export from './deps.ts'.
Also, opine is now in maintenance mode: Deno has introduced Node and NPM compat, considering using Express itself in Deno! -copied from deno.land/x/opine@2.3.3
Great article!
And thanks for the mention 👍
Nice project! Do you mind to share the reason behind the inconsistencies between Express.js and Opine (other than the nature of Deno and Node.js)?
More than happy to - any ones in particular? I'm happy to expand on them 😄
I'd say atm it comes down mostly to time spent on the project, it's quite new and (first commit 9 days ago) and getting the volume of features / API that Express has ported across in a way that allows for early adoption in a short space of time has meant that some things have dropped of the priority. Differences atm fall into 2 buckets:
app.params()
API over yet. These are more complicated, less used (could be wrong) features that aren't essential for a core framework - both could be implemented as custom middleware. I do however very much intend to deliver these - PRs / contributions also welcome!req.body
property in Express on Node vs Opine on Deno. In Node theIncomingMessage
class supports abody
parameter that can be overwritten allowing for body-parsers to drop the parsed body in place. With Deno, thebody
attribute of theServerRequest
class is actually a getter with noset
method so natively there is no way to overwrite it. It does make use of a privatereq._body
which we could use, but that would be breaking all sorts of programming rules. With this particular issue I've paused for now but am fairly certain, if it is highly desired, there is a suitable workaround using a Proxy or more specifically a Revokable Proxy which could act as an interface to the user during middleware execution, and then be revoked to allow the private internals of Deno's std http library to do it's thing undisturbed upon sending of the headers + body. This could be flagged similarly to how theres.headersSent
boolean is implemented in Express.Let me know if there was anything else you wanted to know!
Thanks for the very detailed explanation! That's a wise decision to make the naming inconsistent from the abovementioned points.