Click here to Skip to main content
14,980,986 members
Articles / Programming Languages / Typescript
Article
Posted 3 Jul 2020

Stats

4.9K views
1 bookmarked

Build an In-memory REST API with Deno and Oak

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
3 Jul 2020CPOL8 min read
An overview of building REST API using Deno and Oak
In this article, you will find a step by step approach to create a simple REST API using Deno and Oak.

Welcome to the world of creating a basic REST API using Deno, the most promising server side language based on the Chrome v8 runtime (alternative to node.js).

What is Deno?

If you are familiar with Node.js, the popular server-side JavaScript ecosystem, then Deno is just like Node. Except deeply improved in many ways.

  • It is based on modern features of the JavaScript language.
  • It has an extensive standard library.
  • It has TypeScript at its core, which brings a huge advantage in many different ways, including a first-class TypeScript support (you don’t have to separately compile TypeScript, it’s automatically done by Deno).
  • It embraces ES modules.
  • It has no package manager.
  • It has a first-class await.
  • It has a built-in testing facility.
  • It aims to be browser-compatible as much as it can, for example by providing

Will It Replace Node.js?

No. Node.js is everywhere, well established, incredibly well supported technology that is going to stay for decades.

Deno can be treated as an alternative language to node.js.

Before we delve into the code, let’s take a look at the shape of the API that we will be creating.

For our demo code, we will be creating a basic backend with in memory storage for our “Diagramming Application / Idea Visualization Application”.

Info

Deno uses Typescript by default. We can also use JavaScript if we wish. In this article, we will stick with TypeScript. So, if your TypeScript is rusty, please do a quick revision.

Data Structure

The data structure that represents one record consists of the following attributes:

JavaScript
{ 
   id,
   type,
   text
}

And our data store on the backend is our simple plain old array as shown below:

JavaScript
[
{
  id: "1",
  type: "rect",
  text: "Idea 2 Visuals",
},
{
  id: "2",
  type: "circle",
  text: "Draw",
},
{
  id: "3",
  type: "circle",
  text: "elaborate",
},
{
  id: "4",
  type: "circle",
  text: "experiment",
},
]

Our objective is to create the REST API to do the CRUD operation on the above array (Feel free to put a backend database if curious).

Let’s experiment with the API first, so that you get a feel of the backend we are coding.

Get Request

Image 1

Get by ID

Image 2

Add a Shape (POST)

Set the content-type to application/json as shown (if you are using postman or other tools):

Image 3

Setup the post body:

Image 4

Let's query all shapes to verify whether our newly added record is successfully saved or not.

Image 5

Update a Shape (PUT)

First, let’s verity the shape with an ID of 2.

Image 6

Don’t forget to setup the content type (refer POST section above).

Let’s issue the PUT request to update.

Image 7

Let’s verify the update with a get request:

Image 8

Delete

Let’s delete the record with the ID = 3. Observe the URL parameter we are passing.

Image 9

Let’s Get the Code Rolling

First, install the deno from https://github.com/denoland/deno_install.

One installed, you can verify the installation by running the below command:

deno

The above command should bring up the REPL. Get out of the REPL for now.

A quick deno magic. You can run the program from the URL. For e.g., try the below code:

PowerShell
deno run https://deno.land/std/examples/welcome.ts

And you will get the output as shown below:

Image 10

Let’s Build Our REST API

To build our API, we will use the OAK framework and TypeScript. The Oak is a middleware inspired by Koa framework.

Oak: A middleware framework for Deno’s net server 🦕

https://github.com/oakserver/oak

Fire up your favorite editor and create an app.ts file (as we are using TypeScript) and create the below three files:

  • app.ts
  • routes.ts
  • controller.ts

The app.ts file will be the entry point for our application. The routes.ts defines the REST routes and the controller.ts contains the code for the routes.

Let’s begin by importing the Application object from oak.

TypeScript
import { Application } from 'https://deno.land/x/oak/mod.ts'

The Application class wraps the serve() function from the http package. It has two methods: .use() and .listen(). Middleware is added via the .use() method and the .listen() method will start the server and start processing requests with the registered middleware.

Let’s setup some environment variables to be used by the application, specifically HOST and PORT.

TypeScript
const env = Deno.env.toObject()
const HOST = env.HOST || '127.0.0.1'
const PORT = env.PORT || 7700

The next step is to create an instance of the Application and start our server. Though note that our server will not run because we will need a middleware to process our request (as we will be using Oak).

JavaScript
const app = new Application();
// routes config goes here

console.log(`Listening on port ${PORT}...`)
await app.listen(`${HOST}:${PORT}`)

Here, we create a new Application instance and listen on the app object at specific host and port.

The next step is create the routes and the controller (NOTE: The controller is not mandatory, but we are segregating the code as per responsibility.)

Routes

The route code is pretty self explanatory. Do note that to keep the code clean, the actual request/response handling code is loaded from controller.ts.

NOTE:

Deno uses URL for importing modules.

TypeScript
import { Router } from 'https://deno.land/x/oak/mod.ts'
import { getShapes, getShape, addShape,  updateShape, deleteShape 
} from './controller.ts'

const router = new Router()
router.get('/shapes', getShapes)
      .get('/shapes/:id', getShape)
      .post('/shapes', addShape)
      .put('/shapes/:id', updateShape)
      .delete('/shapes/:id', deleteShape)

export default router

Here, we first import the {Router} from the oak package. To make this code work, we have to create the getShapes, getShape, addShape, updateShape, deleteShape methods in or controller.ts which we will shortly do.

Then we create an instance of the router and hook onto the get, post, put and delete method. Dynamic query string parameters are denoted by “:”.

And finally, we export the router so that other modules can import it.

Now before we start with the final piece, controller.ts, let's fill in the remaining part of the code for our app.ts as shown below:

TypeScript
import { Application } from 'https://deno.land/x/oak/mod.ts'
import router from './routes.ts'

const env = Deno.env.toObject()
const HOST = env.HOST || '127.0.0.1'
const PORT = env.PORT || 7700

const app = new Application();

app.use(router.routes())
app.use(router.allowedMethods())

console.log(`Listening on port ${PORT}...`)
await app.listen(`${HOST}:${PORT}`)

Here, we import routes file using the import statement and passing in relative path.

TypeScript
import router from './routes.ts'

Then, we load up the middleware by the below method call:

TypeScript
app.use(router.routes())
app.use(router.allowedMethods())

allowedMethods

The allowedMethods needs to passed as per oak documentation. It takes a parameter as well which can be further customized.

Let’s start with the controller code.

controller.ts

We begin by creating the interface for our model. We will call it as IShape as we are dealing with drawing/diagramming application.

TypeScript
interface IShape {
  id: string;
  type: string;
  text: string;
}

Let’s simulate an in-memory storage. And of course array shines here. So, we will create an array with some sample data.

TypeScript
let shapes: Array<IShape> = [
  {
    id: "1",
    type: "rect",
    text: "Idea 2 Visuals",
  },
  {
    id: "2",
    type: "circle",
    text: "Draw",
  },
  {
    id: "3",
    type: "circle",
    text: "elaborate",
  },
  {
    id: "4",
    type: "circle",
    text: "experiment",
  },
]

Now, before we begin with the main API code, let’s code and helper method to fetch record using ID as the parameter. This method will be used in the delete and update methods.

TypeScript
// Helper method to find record by id
const findById = (id: string): ( IShape | undefined ) =>{
  return shapes.filter(shape => shape.id === id )[0]
}

Here, we simply use the good old array filter method. But as you may be aware, filter method always returns an array, even if there is only one outcome, we use the [0] to grab the first element.

TIP

It’s a good idea to have a consistent method naming and return value convention for our methods. findById in most frameworks and library is known to return only one record/block.

Now having done the preliminary work, let’s begin our API by implementing the “GET” method. If you recollect from our router discussion, a request to /shapes url is expecting a method getShapes to be invoked.

A Note on Request/Response

By default, all Oak request has access to the context object. The context object exposes the below important properties:

  • app – a reference to the Application that is invoking this middleware
  • request – the Request object which contains details about the request
  • response – the Response object which will be used to form the response sent ack to the requestor/client
  • params – added by the route
  • state – a record of application state

GET /shapes

The getShapes method is quite simple. If we had a real database here, then you will simply fetch all the records (or records as per paging) in this method.

But in our case, the array, shapes, is the data store. We return data back to the caller by setting the response.body to the data that is to be returned, there the complete “shapes” array.

TypeScript
const getShapes = ({ response }: { response: any }) => { 
  response.body = shapes 
}

response

The response object is passed by the http/oak framework to us. This object will be used to send the response back to the requestor. It also passes a request object, which we will shortly examine.

GET /shapes/2

A good API server should also enable to fetch only selective record. This is where this second GET method comes into play. Here, we pass in the parameter, id, as query string to the /shapes route.

TypeScript
const getShape = ({ params, response }: { params: { id: string }; response: any }) => {
  const shape: IShape | undefined = findById(params.id)
  if (shape) {
    response.status = 200
    response.body = shape
  } else {
    response.status = 404
    response.body = { message: `Shape not found.` }
  }   
}

In the getShape method, we destructure the params and a response object. If the shape is found, we send a 200OK status along with the shape in response body. Otherwise a 404 error.

POST /shapes

In the REST world, to create a new resource/record, we use the POST method. The POST method receives its parameter in the body rather than the URL.

Let’s take a look at the code:

TypeScript
// Create a new shape
const addShape = async ({request, response}: {request: any; response: any}) => {
  const body  = await request.body()
  const shape: IShape = body.value
  shapes.push(shape)
  response.body = {
    message: "OK"
  }
  response.status = 200
}

Note, we use await request.body() as the body() method is async. We grab the value and push it back to the array and then respond with an OK message.

PUT /shapes (The update)

The PUT/PATCH method is used to update a record/entity.

TypeScript
// Update an existing shape data
const updateShape =  async ({ params, request, response }: 
                     { params: { id: string }; request: any; response: any }) => {
  let shape: IShape | undefined = findById(params.id)
  if (shape) {
    const body = await request.body()
    const updates: { type?: string; text?: string } = body.value
    shape = {...shape, ...updates} // update 

    // Update the shape back to array
    shapes = [...shapes.filter(s => s.id !== params.id), shape]

    response.status = 200
    response.body = {
      message: "OK"
    } 
  } else {
    response.status = 404;
    response.body = {
      message: "Shape not found"
    }
  }
}

The update method seems quite involved but is quite simple. To simply let me outline the process.

  1. Grab the entity/resource to be edited by its ID
  2. If found, get the body of the request that contains updated data in JSON form (set content-type: application/json in the request header)
  3. Get the updated hash values.
  4. Merge the currently found shape object with the updated value.
  5. At this point, the “shape” variable contains the latest updates.
  6. Merge the updated “shape” back to the “shapes” array.
  7. Send back the response to the client.

And now, finally, let’s take a look at the DELETE method.

DELETE (/shapes)

The delete method is quite simple. Grab the shape to be deleted by using the ID. In this case, we just filter out the record which is deleted.

TypeScript
// Delete a shape by it's ID
const deleteShape = ({ params, response }: { params: { id: string }; response: any }) => {
  shapes = shapes.filter(shape => shape.id !== params.id)
  response.body = { message: 'OK' }
  response.status = 200
}

NOTE: In a real world project, do the needed validations on the server.

Run the server:

PowerShell
deno run --allow-env --allow-net  app.ts

NOTE: For security reasons, Deno does not allow programs to access the network without explicit permission. To allow accessing the network, use a command-line flag:

And you have a running REST API server in deno. 🦕

The complete source code can be found at https://github.com/rajeshpillai/deno-inmemory-rest-api.

History

  • 3rd July, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Rajesh Pillai
Founder Algorisys Technologies Pvt. Ltd.
India India
Co Founder at Algorisys Technologies Pvt. Ltd.

http://algorisys.com/
https://teachyourselfcoding.com/ (free early access)
https://www.youtube.com/user/tekacademylabs/

Comments and Discussions

 
-- There are no messages in this forum --