DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Create a Simple REST API with DENO and OAK framework
Nickolas Benakis
Nickolas Benakis

Posted on

Create a Simple REST API with DENO and OAK framework

About Deno

Deno has just recently released version 1.0.0 and this is a new secure runtime for JavaScript and Typescript. Main key features of Deno are:

  • Secure by default. No file, network, or environment access, unless explicitly enabled.
  • Supports TypeScript out of the box.
  • Ships only a single executable file.
  • Has built-in utilities like a dependency inspector (deno info) and a code formatter (deno fmt).
  • Has a set of reviewed (audited) standard modules that are guaranteed to work with Deno: deno.land/std
  • If you’d like to learn more about Deno, please check out Deno’s landing page.

    About Oak

    Oak is middleware framework for Deno's http server, including a router middleware. This middleware framework is inspired by Koa and middleware router inspired by koa-router. For more Info check out here

    Let's start

    I'll be installing Deno using Brew.

    brew install deno
    

    To verify if the Deno is installed or not. Just type deno --version on your terminal and it will show the install version of Deno.

    $ Deno --version
    deno 1.0.0
    v8 8.4.300
    typescript 3.9.2
    

    After the installation steps, let's create a directory for our application

    mkdir denoRestApi && cd denoRestApi
    

    We are going to develop a Crud REST api with the following structure

    - src
      - controllers
        - dinosaurs.ts
      - models
        - dinosaurs.ts
      - routes
        - routes.ts
      - types
        - types.ts
    - server.ts
    
    • Controllers : have a logic of the application and handle the client requests.
    • Models : contain the model definition.
    • Routes : containing API routes.
    • Types : contain the types used by model and application responses.
    • Server : code to run localhost server.

    Now let’s create our server.ts file in the root of our directory :

    import { Application } from "https://deno.land/x/oak/mod.ts";
    import router from "./src/routes/routes.ts";
    
    const port = 9000;
    const app = new Application();
    
    app.use(router.routes());
    app.use(router.allowedMethods());
    
    console.log(`Server running on port ${port}`);
    await app.listen({ port });
    

    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.

    The middleware is processed as a stack, where each middleware function can control the flow of the response. When the middleware is called, it is passed a context and reference to the "next" method in the stack.

    Our Next step is to create our endpoints in our routes.ts :

    import { Router } from "https://deno.land/x/oak/mod.ts";
    import {
      getDinosaur,
      getDinosaurs,
      addDinosaur,
      updateDinosaur,
      deleteDinosaur,
    } from "../controllers/dinosaurs.ts";
    
    const router = new Router();
    
    router.get("/api/v1/dinosaurs", getDinosaurs)
      .get("/api/v1/dinosaurs/:id", getDinosaur)
      .post("/api/v1/dinosaurs", addDinosaur)
      .put("/api/v1/dinosaurs/:id", updateDinosaur)
      .delete("/api/v1/dinosaurs/:id", deleteDinosaur);
    
    export default router;
    

    One of TypeScript’s core principles is that type checking focuses on the shape that values have. This is sometimes called β€œduck typing” or β€œstructural subtyping”. In TypeScript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project. See below types.ts :

     export interface Dinosaur {
       id: string;
       name: string;
       era: string;
       area: string;
       diet: string;
     }
    

    Properties inspired by here

    Now let's create our initial list with dinosaurs in models/dinosaurs.ts :

    import { Dinosaur } from "../types/types.ts";
    
    export const Dinosaurs: Array<Dinosaur> = [
      {
        id: "1",
        name: "Achillobator",
        era: "Late Cretaceous",
        area: "Mongolia",
        diet: "carnivorous",
      },
      {
        id: "2",
        name: "Agilisaurus",
        era: "Late Jurassic",
        area: "China",
        diet: "herbivorous",
      },
      {
        id: "3",
        name: "Melanorosaurus",
        era: "Late Triassic",
        area: "South Africa",
        diet: "omnivorous",
      },
    ];
    

    After we have created our Dinosaur Interface, our dinosaur list and our routes, let's continue with our methods for each endpoint. controllers/dinosaurs.ts :

    import { v4 } from "https://deno.land/std/uuid/mod.ts";
    import {
      Dinosaur,
    } from "../types/types.ts";
    import { Dinosaurs } from "../models/dinosaurs.ts";
    
    const getDinosaurs = ({ response }: { response: any }) => {
      response.body = {
        success: true,
        data: Dinosaurs,
      };
    };
    
    const getDinosaur = (
      { params, response }: { params: { id: string }; response: any },
    ) => {
      const selectedDino: Dinosaur | undefined = Dinosaurs.find((dino) =>
        dino.id === params.id
      );
      if (selectedDino) {
        response.status = 200;
        response.body = {
          success: true,
          data: selectedDino,
        };
      } else {
        response.status = 404;
        response.body = {
          success: false,
          msg: "Dinosaur Not Found",
        };
      }
    };
    
    const addDinosaur = async (
      { request, response }: { request: any; response: any },
    ) => {
      if (!request.hasBody) {
        response.status = 400;
        response.body = {
          success: false,
          msg: "No data",
        };
      } else {
        const { value : dinosaurBody } = await request.body();
        const dinosaur: Dinosaur = dinosaurBody;
        dinosaur.id = v4.generate();
        Dinosaurs.push(dinosaur);
        response.status = 201;
        response.body = {
          success: true,
          data: dinosaur,
        };
      }
    };
    
    const deleteDinosaur = (
      { params, response }: { params: { id: string }; response: any },
    ) => {
      const filteredDinosaurs: Array<Dinosaur> = Dinosaurs.filter(
        (dinosaur: Dinosaur) => (dinosaur.id !== params.id),
      );
      if (filteredDinosaurs.length === Dinosaurs.length) {
        response.status = 404;
        response.body = {
          success: false,
          msg: "Not found",
        };
      } else {
        Dinosaurs.splice(0, Dinosaurs.length);
        Dinosaurs.push(...filteredDinosaurs);
        response.status = 200;
        response.body = {
          success: true,
          msg: `Dinosaur with id ${params.id} has been deleted`,
        };
      }
    };
    
    const updateDinosaur = async (
      { params, request, response }: {
        params: { id: string };
        request: any;
        response: any;
      },
    ) => {
      const requestedDinosaur: Dinosaur | undefined = Dinosaurs.find(
        (dinosaur: Dinosaur) => dinosaur.id === params.id,
      );
      if (requestedDinosaur) {
        const { value : updatedDinosaurBody } = await request.body();
        const updatedDinosaurs: Array<Dinosaur> = Dinosaurs.map(
          (dinosaur: Dinosaur) => {
            if (dinosaur.id === params.id) {
              return {
                ...dinosaur,
                ...updatedDinosaurBody,
              };
            } else {
              return dinosaur;
            }
          },
        );
    
        Dinosaurs.splice(0, Dinosaurs.length);
        Dinosaurs.push(...updatedDinosaurs);
        response.status = 200;
        response.body = {
          success: true,
          msg: `Dinosaur id ${params.id} updated`,
        };
      } else {
        response.status = 404;
        response.body = {
          success: false,
          msg: `Not Found`,
        };
      }
    };
    
    export {
      updateDinosaur,
      deleteDinosaur,
      getDinosaurs,
      getDinosaur,
      addDinosaur,
    };
    

    Run application

    Deno  run --allow-net  server.ts
    

    Request with curl

    Resolution

    We've created a dead simple, readable rest api with few lines of code. If you noticed we did not used any Node_modules dependancies instead Deno has an amazing list of features at Standard Library and Third-Party Modules. I like Deno so far and I am very excited about the out-of-box tools it provides.

    You can find my repo here. Leave a 🌟 if you liked it.

    Thanks a lot,
    Don't hesitate to write any comments below, I would love to answer.

    Feel free to connect on:

    Top comments (3)

    Collapse
     
    coyotte508 profile image
    coyotte508 • Edited on

    A few things I learned from Koa (from which Oak is inspired):

    • You don't need to set the status of the response to 200, if you set its body to something
    • You don't need to send a 404, just return without touching response.body

    You can set a middleware to handle error http codes, if you want to send a JSON payload along with the status, without specifying the error payload in each of your routes:

    app.use(async (ctx, next) => {
      await next();
    
      if (ctx.response.status >= 400) {
        let message = "Error";
        switch (ctx.response.status) {
          case 404: message = "Not Found"; break;
        }
        ctx.response.body = {success: false, message};
      }
    });
    
    Enter fullscreen mode Exit fullscreen mode

    All that should simplify the code of your router handlers a fair bit :)

    Collapse
     
    hviana profile image
    Henrique Emanoel Viana

    I created middleware for uploading files:
    deno.land/x/upload_middleware_for_...

    Collapse
     
    nickolasbenakis profile image
    Nickolas Benakis

    awesome !

    typescript

    11 Tips That Make You a Better Typescript Programmer

    1 Think in {Set}

    Type is an everyday concept to programmers, but it’s surprisingly difficult to define it succinctly. I find it helpful to use Set as a conceptual model instead.

    #2 Understand declared type and narrowed type

    One extremely powerful typescript feature is automatic type narrowing based on control flow. This means a variable has two types associated with it at any specific point of code location: a declaration type and a narrowed type.

    #3 Use discriminated union instead of optional fields

    ...

    Read the whole post now!