DEV Community

Cover image for Create your first RESTful API with Deno, Oak and MongoDB
Nedwize
Nedwize

Posted on • Edited on

Create your first RESTful API with Deno, Oak and MongoDB

In this article we are going to create a RESTful API with CRUD functionalities. We're going to talk about Deno in detail and build a server using Oak, which is a minimalistic middleware framework (similar to Express).

Prerequisites

At least some basic knowledge of these technologies / concepts is required.

  • ES6 Features
  • TypeScript
  • RESTful APIs
  • MongoDB

What is Deno? 🦕

Similar to Node.js, Deno is a JavaScript / TypeScript runtime based on the V8 JavaScript Engine. It was created by Ryan Dahl (also the creator of Node.js).

Why should we use Deno?

  • Built-in support for TypeScript
  • Runs in a sandbox and is secure by default
  • De-centralized packages
  • ES Modules
  • Top Level Await

Installation ⚙️

Using Shell (macOS and Linux):
curl -fsSL https://deno.land/x/install/install.sh | sh

Using PowerShell (Windows):
iwr https://deno.land/x/install/install.ps1 -useb | iex

For more installation options checkout the documentation

To test your installation, run deno --version. If this prints the Deno version to the console the installation was successful.

Let's start building

We are going to create a quotes API which will provide us endpoints to perform CRUD operations on these quotes.

Create a server.ts and bring in Application from Oak. Now create a new Application

import { Application } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
Enter fullscreen mode Exit fullscreen mode

Now start the server with

const PORT = 4000;
await app.listen({ port: PORT });
console.log(`Server running on PORT: ${PORT}`)
Enter fullscreen mode Exit fullscreen mode

We will keep our code clean and separated in modules. Let's make a routes.ts file to define our routes. We will come back to the server.ts file soon. In the routes.ts file - Bring in Router from Oak

import { Router } from "https://deno.land/x/oak/mod.ts";

const router = new Router(); // Create Router

router
  .get("/api/quote", getQuotes) // Get all quotes
  .get("/api/quote/:id", getQuote) // Get one quote of quoteID: id
  .post("/api/quote", addQuote) // Add a quote
  .put("/api/quote/:id", updateQuote) // Update a quote
  .delete("/api/quote/:id", deleteQuote); // Delete a quote

export default router;
Enter fullscreen mode Exit fullscreen mode

At this point, these functions passed in the routes will be undefined, we'll come back to that later. First, let's import our router in the server.ts file.

import router from "./routes.ts"; // Bringing in router

app.use(router.routes()); // Pass our router as a middleware
app.use(router.allowedMethods()); // Allow HTTP methods on router
Enter fullscreen mode Exit fullscreen mode

Our server.ts should look like this

import { Application } from "https://deno.land/x/oak/mod.ts";
import router from "./routes.ts";
const PORT = 4000;

const app = new Application();

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

await app.listen({ port: PORT });
console.log(`Server listening on ${PORT}`);
Enter fullscreen mode Exit fullscreen mode

Now, let's build our controllers. Create a controllers directory in the root of your project. Then create controllers.js -

First, create an interface for a quote

interface Quote {
  _id: { $oid: string };
  quote: string;
  quoteID: string;
  author: string;
}
Enter fullscreen mode Exit fullscreen mode

Next, we'll bring in MongoClient from Deno's Mongo Library in our controllers.ts and try to connect with it. Make sure you have a MongoDB Local instance running. You can also use MongoDB Atlas, just replace your URI with the one here.

import { MongoClient } from "https://deno.land/x/mongo@v0.22.0/mod.ts";
const URI = "mongodb://127.0.0.1:27017";

// Mongo Connection Init
const client = new MongoClient();
try {
  await client.connect(URI);
  console.log("Database successfully connected");
} catch (err) {
  console.log(err);
}

const db = client.database("quotesApp"); 
const quotes = db.collection<Quote>("quotes");
Enter fullscreen mode Exit fullscreen mode

Let's get to the fun part now, we'll create our first controller - addQuote to add a quote to the database.

// DESC: ADD single quote
// METHOD: POST /api/quote
const addQuote = async ({
  request,
  response,
}: {
  request: any;
  response: any;
}) => {
  try {
    // If the request has no Body, it will return a 404
    if (!request.hasBody) { 
      response.status = 400;
      response.body = {
        success: false,
        msg: "No Data",
      };
    } else {
      // Otherwise, it will try to insert 
      // a quote in the DB and respond with 201
      const body = await request.body();
      const quote = await body.value;
      await quotes.insertOne(quote);
      response.status = 201;
      response.body = {
        success: true,
        data: quote,
      };
    }
  } catch (err) {
    response.body = {
      success: false,
      msg: err.toString(),
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

Next, we move on to the getQuote function which returns a quote from the database.

// DESC: GET single quote
// METHOD: GET /api/quote/:id
const getQuote = async ({
  params,
  response,
}: {
  params: { id: string };
  response: any;
}) => {
  // Searches for a particular quote in the DB
  const quote = await quotes.findOne({ quoteID: params.id });
  // If found, respond with the quote. If not, respond with a 404
  if (quote) {
    response.status = 200;
    response.body = {
      success: true,
      data: quote,
    };
  } else {
    response.status = 404;
    response.body = {
      success: false,
      msg: "No quote found",
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

To get all the quotes stored in the database, we create the getQuotes function.

// DESC: GET all Quotes
// METHOD GET /api/quote
const getQuotes = async ({ response }: { response: any }) => {
  try {
    // Find all quotes and convert them into an Array
    const allQuotes = await quotes.find({}).toArray();
    console.log(allQuotes);
    if (allQuotes) {
      response.status = 200;
      response.body = {
        success: true,
        data: allQuotes,
      };
    } else {
      response.status = 500;
      response.body = {
        success: false,
        msg: "Internal Server Error",
      };
    }
  } catch (err) {
    response.body = {
      success: false,
      msg: err.toString(),
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

For updating a particular quote, let's build the updateQuote function.

// DESC: UPDATE single quote
// METHOD: PUT /api/quote/:id
const updateQuote = async ({
  params,
  request,
  response,
}: {
  params: { id: string };
  request: any;
  response: any;
}) => {
  try {
    // Search a quote in the DB and update with given values if found
    const body = await request.body();
    const inputQuote = await body.value;
    await quotes.updateOne(
      { quoteID: params.id },
      { $set: { quote: inputQuote.quote, author: inputQuote.author } }
    );
    // Respond with the Updated Quote
    const updatedQuote = await quotes.findOne({ quoteID: params.id });
    response.status = 200;
    response.body = {
      success: true,
      data: updatedQuote,
    };
  } catch (err) {
    response.body = {
      success: false,
      msg: err.toString(),
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

Now, creating the deleteQuote function.

// DESC: DELETE single quote
// METHOD: DELETE /api/quote/:id
const deleteQuote = async ({
  params,
  response,
}: {
  params: { id: string };
  request: any;
  response: any;
}) => {
  try {
    // Search for the given quote and drop it from the DB
    await quotes.deleteOne({ quoteID: params.id });
    response.status = 201;
    response.body = {
      success: true,
      msg: "Product deleted",
    };
  } catch (err) {
    response.body = {
      success: false,
      msg: err.toString(),
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

Let's export all these functions so we can use them in routes.ts

export { getQuotes, getQuote, addQuote, updateQuote, deleteQuote };
Enter fullscreen mode Exit fullscreen mode

🚩 We're almost done. Next, we'll bring in the controller.ts file in our router. Just add this line under the import statements in the routes.ts file.

import {
  addQuote,
  getQuotes,
  getQuote,
  updateQuote,
  deleteQuote,
} from "./controllers/quotes.ts";
Enter fullscreen mode Exit fullscreen mode

To run the server, go to your project directory and execute this command
deno run --allow-all server.ts

Thats it! Your API should be running. Test it with Postman or RestClient

You can view the entire code here
Drop a ⭐ on the repo if you like it. Feel free to open any issue or drop a comment if you face any problems or find a mistake somewhere.

If you like my content, please drop a follow or connect with me on my socials
Github - @NakshatraCodes
LinkedIn - Nakshatra Saxena

Top comments (1)

Collapse
 
ericburel profile image
Eric Burel

It would be interesting to see how you structure the app at scale, because here you seem to define "db" and each collection at top level. If you want to split the business code, do you pass the "db" or "collection" object to function? Do you create classes that uses the db as a constructor?

Mongoose bypasses this issue by letting you call functions before the database is connected, so you don't have to wait for the "db" object to actually exist, but this is not possible with the raw mongo driver.

This issue is the same with Deno or Node btw