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:

    Discussion (3)

    Collapse
    coyotte508 profile image
    coyotte508 • Edited

    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 Author

    awesome !