# Getting Started with Deno & MongoDB

Deno is a “modern” runtime for JavaScript and TypeScript that is built in Rust. This makes it very fast! 

If you are familiar with Node.js then you will be right at home with Deno. It is very similar but has some improvements over Node.js. In fact, the creator of Deno, Ryan Dahl, also created Node and Deno is meant to be the successor to Node.js.

> 💡 Fun Fact: Deno is an anagram. Rearrange the letters in Node to spell Deno.

Deno has no package manager, uses ES modules, has first-class `await`, has built-in testing, and is somewhat browser-compatible by providing built-in `fetch` and the global `window` object. 

Aside from that, it’s also very secure. It’s completely locked down by default and requires you to enable each access method specifically. 

This makes Deno pair nicely with MongoDB since it is also super secure by default. 

### Video Version

Here is a video version of this article if you prefer to watch.
:youtube]{vid=xOgicDUXnrE}

## Prerequisites

- TypeScript — Deno uses TypeScript by default, so some TypeScript knowledge is needed.
- Basic MongoDB knowledge
- Understanding of RESTful APIs

## Getting Deno set up

You’ll need to install Deno to get started.

- macOS and Linux Shell:
    
  `curl -fsSL https://deno.land/install.sh | sh`
    
- Windows PowerShell:
    
  `iwr https://deno.land/install.ps1 -useb | iex`
    

For more options, here are the Deno [installation instructions.

## Setting up middleware and routing

For this tutorial, we’re going to use Oak, a middleware framework for Deno. This will provide routing for our various app endpoints to perform CRUD operations. 

We’ll start by creating a `server.ts` file and import the Oak `Application` method. 

```jsx
import { Application } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
```

> 💡 If you are familiar with Node.js, you’ll notice that Deno does things a bit differently. Instead of using a `package.json` file and downloading all of the packages into the project directory, Deno uses file paths or URLs to reference module imports. Modules do get downloaded and cached locally, but this is done globally and not per project. This eliminates a lot of the bloat that is inherent from Node.js and its `node_modules` folder.

Next, let’s start up the server.

```jsx
const PORT = 3000;
app.listen({ port: PORT });
console.log(`Server listening on port ${PORT}`);
```

We’re going to create our routes in a new file named `routes.ts`. This time, we’ll import the `Router` method from Oak. Then, create a new instance of `Router()` and export it. 

```jsx
import { Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router(); // Create router

export default router;
```

Now, let’s bring our `router` into our `server.ts` file.

```jsx
import { Application } from "https://deno.land/x/oak/mod.ts";
import router from "./routes.ts"; // Import our router

const PORT = 3000;
const app = new Application();

app.use(router.routes()); // Implement our router
app.use(router.allowedMethods()); // Allow router HTTP methods

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

## Setting up the MongoDB Data API

In most tutorials, you’ll find that they use the mongo third-party Deno module. For this tutorial, we’ll use the brand new MongoDB Atlas Data API to interact with our MongoDB Atlas database in Deno. The Data API doesn’t require any drivers!

Let’s set up our MongoDB Atlas Data API. You’ll need a MongoDB Atlas account. If you already have one, sign in, or register now.

From the MongoDB Atlas Dashboard, click on the Data API option. By default, all MongoDB Atlas clusters have the Data API turned off. Let’s enable it for our cluster. You can turn it back off at any time. 

After enabling the Data API, we’ll need to create an API Key. You can name your key anything you want. I’ll name mine `data-api-test`. Be sure to copy your API key secret at this point. You won’t see it again!

Also, take note of your App ID. It can be found in your URL Endpoint for the Data API. 

Example: `https://data.mongodb-api.com/app/{APP_ID}/endpoint/data/beta`

## Configuring each route

At this point, we need to set up each function for each route. These will be responsible for Creating, Reading, Updating, and Deleting (CRUD) documents in our MongoDB database.

Let’s create a new folder called `controllers` and a file within it called `todos.ts`.

Next, we’ll set up our environmental variables to keep our secrets safe. For this, we’ll use a module called dotenv. 

```jsx
import { config } from "https://deno.land/x/dotenv/mod.ts";
const { DATA_API_KEY, APP_ID } = config();
```

Here, we are importing the `config` method from that module and then using it to get our `DATA_API_KEY` and `APP_ID` environmental variables. Those will be pulled from another file that we’ll create in the root of our project called `.env`. Just the extension and no file name.

```
DATA_API_KEY=your_key_here
APP_ID=your_app_id_here
```

This is a plain text file that allows you to store secrets that you don’t want to be uploaded to GitHub or shown in your code. To ensure that these don’t get uploaded to GitHub, we’ll create another file in the root of our project called `.gitignore`. Again, just the extension with no name.

```
.env
```

In this file, we’ll simply enter `.env`. This lets git know to ignore this file so that it’s not tracked.

Now, back to the `todos.ts` file. We’ll configure some variables that will be used throughout each function.

```jsx
const BASE_URI = `https://data.mongodb-api.com/app/${APP_ID}/endpoint/data/beta/action`;
const DATA_SOURCE = "Cluster0";
const DATABASE = "todo_db";
const COLLECTION = "todos";

const options = {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "api-key": DATA_API_KEY 
  },
  body: ""
};
```

We’ll set up our base URI to our MongoDB Data API endpoint. This will utilize our App ID. Then we need to define our data source, database, and collection. These would be specific to your use case. And lastly, we will define our fetch options, passing in our Data API key.

## Create route

Now we can finally start creating our first route function. We’ll call this function `addTodo`. This function will add a new todo item to our database collection.

```jsx
const addTodo = async ({
  request,
  response,
}: {
  request: any;
  response: any;
}) => {
  try {
    if (!request.hasBody) {
      response.status = 400;
      response.body = {
        success: false,
        msg: "No Data",
      };
    } else {
      const body = await request.body();
      const todo = await body.value;
      const URI = `${BASE_URI}/insertOne`;
      const query = {
        collection: COLLECTION,
        database: DATABASE,
        dataSource: DATA_SOURCE,
        document: todo
      };
      options.body = JSON.stringify(query);
      const dataResponse = await fetch(URI, options);
      const { insertedId } = await dataResponse.json();
      
      response.status = 201;
      response.body = {
        success: true,
        data: todo,
        insertedId
      };
    }
  } catch (err) {
    response.body = {
      success: false,
      msg: err.toString(),
    };
  }
};

export { addTodo };
```

This function will accept a `request` and `response`. If the `request` doesn’t have a `body` it will return an error. Otherwise, we’ll get the `todo` from the `body` of the `request` and use the `insertOne` Data API endpoint to insert a new document into our database.

We do this by creating a `query` that defines our `dataSource`, `database`, `collection`, and the `document` we are adding. This gets stringified and sent using `fetch`. `fetch` happens to be built into Deno as well; no need for another module like in Node.js.

We also wrap the entire function contents with a `try..catch` to let us know if there are any errors. 

As long as everything goes smoothly, we’ll return a status of `201` and a `response.body`.

Lastly, we’ll export this function to be used in our `routes.ts` file. So, let’s do that next.

```jsx
import { Router } from "https://deno.land/x/oak/mod.ts";
import { addTodo } from "./controllers/todos.ts"; // Import controller methods

const router = new Router();

// Implement routes
router.post("/api/todos", addTodo); // Add a todo

export default router;
```

### Testing the create route

Let’s test our create route. To start the server, we’ll run the following command:

`deno run --allow-net --allow-read server.ts`

> 💡 Like mentioned before, Deno is locked down by default. We have to specify that network access is okay by using the `--allow-net` flag, and that read access to our project directory is okay to read our environmental variables using the `--allow-read` flag.

Now our server should be listening on port 3000. To test, we can use Postman, Insomnia, or my favorite, the Thunder Client extension in VS Code.

We’ll make a `POST` request to `localhost:3000/api/todos` and include in the `body` of our request the json document that we want to add.

```json
{
  "title": "Todo 1",
  "complete": false,
  "todoId": 1
}
```

> 💡 Normally, I would not create an ID manually. I would rely on the MongoDB generated ObjectID, `_id`. That would require adding another Deno module to this project to convert the BSON ObjectId. I wanted to keep this tutorial as simple as possible. 

If all goes well, we should receive a successful response.

## Read all documents route

Now let’s move on to the read routes. We’ll start with a route that gets all of our todos called `getTodos`. 

```jsx
const getTodos = async ({ response }: { response: any }) => {
  try {
    const URI = `${BASE_URI}/find`;
    const query = {
      collection: COLLECTION,
      database: DATABASE,
      dataSource: DATA_SOURCE
    };
    options.body = JSON.stringify(query);
    const dataResponse = await fetch(URI, options);
    const allTodos = await dataResponse.json();

    if (allTodos) {
      response.status = 200;
      response.body = {
        success: true,
        data: allTodos,
      };
    } else {
      response.status = 500;
      response.body = {
        success: false,
        msg: "Internal Server Error",
      };
    }
  } catch (err) {
    response.body = {
      success: false,
      msg: err.toString(),
    };
  }
};
```

This one will be directed to the `find` Data API endpoint. We will not pass anything other than the `dataSource`, `database`, and `collection` into our `query`. This will return all documents from the specified collection.

Next, we’ll need to add this function into our exports at the bottom of the file.

```jsx
export { addTodo, getTodos }
```

Then we’ll add this function and route into our `routes.ts` file as well.

```jsx
import { Router } from "https://deno.land/x/oak/mod.ts";
import { addTodo, getTodos } from "./controllers/todos.ts"; // Import controller methods

const router = new Router();

// Implement routes
router
  .post("/api/todos", addTodo) // Add a todo
  .get("/api/todos", getTodos); // Get all todos

export default router;
```

### Testing the read all documents route

Since we’ve made changes, we’ll need to restart our server using the same command as before:

`deno run --allow-net --allow-read server.ts`

To test this route, we’ll send a `GET` request to `localhost:3000/api/todos` this time, with nothing in our request `body`.

This time, we should see the first document that we inserted in our response.

## Read a single document route

Next, we’ll set up our function to read a single document. We’ll call this one `getTodo`.

```jsx
const getTodo = async ({
  params,
  response,
}: {
  params: { id: string };
  response: any;
}) => {
  const URI = `${BASE_URI}/findOne`;
  const query = {
    collection: COLLECTION,
    database: DATABASE,
    dataSource: DATA_SOURCE,
    filter: { todoId: parseInt(params.id) }
  };
  options.body = JSON.stringify(query);
  const dataResponse = await fetch(URI, options);
  const todo = await dataResponse.json();
  
  if (todo) {
    response.status = 200;
    response.body = {
      success: true,
      data: todo,
    };
  } else {
    response.status = 404;
    response.body = {
      success: false,
      msg: "No todo found",
    };
  }
};
```

This function will utilize the `findOne` Data API endpoint and we’ll pass a `filter` this time into our `query`. 

We’re going to use query `params` from our URL to get the ID of the document we will filter for. 

Next, we need to export this function as well.

```jsx
export { addTodo, getTodos, getTodo }
```

And, we’ll import the function and set up our route in the `routes.ts` file.

```jsx
import { Router } from "https://deno.land/x/oak/mod.ts";
import { 
  addTodo, 
  getTodos, 
  getTodo 
} from "./controllers/todos.ts"; // Import controller methods

const router = new Router();

// Implement routes
router
  .post("/api/todos", addTodo) // Add a todo
  .get("/api/todos", getTodos) // Get all todos
  .get("/api/todos/:id", getTodo); // Get one todo

export default router;
```

### Testing the read single document route

Remember to restart the server. This route is very similar to the “read all documents” route. This time, we will need to add an ID to our URL. Let’s use: `localhost:3000/api/todos/1`.

We should see the document with the `todoId` of 1. 

> 💡 To further test, try adding more test documents using the `POST` method and then run the two `GET` methods again to see the results.

## Update route

Now that we have documents, let’s set up our update route to allow us to make changes to existing documents. We’ll call this function `updateTodo`.

```jsx
const updateTodo = async ({
  params,
  request,
  response,
}: {
  params: { id: string };
  request: any;
  response: any;
}) => {
  try {
    const body = await request.body();
    const { title, complete } = await body.value;
    const URI = `${BASE_URI}/updateOne`;
    const query = {
      collection: COLLECTION,
      database: DATABASE,
      dataSource: DATA_SOURCE,
      filter: { todoId: parseInt(params.id) },
      update: { $set: { title, complete } }
    };
    options.body = JSON.stringify(query);
    const dataResponse = await fetch(URI, options);
    const todoUpdated = await dataResponse.json();
    
    response.status = 200;
    response.body = { 
      success: true,
      todoUpdated 
    };
    
  } catch (err) {
    response.body = {
      success: false,
      msg: err.toString(),
    };
  }
};
```

This route will accept three arguments: `params`, `request`, and `response`. The `params` will tell us which document to update, and the `request` will tell us what to update.

We’ll use the `updateOne` Data API endpoint and set a `filter` and `update` in our `query`.

The `filter` will indicate which document we are updating and the `update` will use the `$set` operator to update the document fields. 

The updated data will come from our `request.body`.

Let’s export this function at the bottom of the file.

```jsx
export { addTodo, getTodos, getTodo, updateTodo }
```

And, we’ll import the function and set up our route in the `routes.ts` file.

```jsx
import { Router } from "https://deno.land/x/oak/mod.ts";
import { 
  addTodo, 
  getTodos, 
  getTodo,
  updateTodo
} from "./controllers/todos.ts"; // Import controller methods

const router = new Router();

// Implement routes
router
  .post("/api/todos", addTodo) // Add a todo
  .get("/api/todos", getTodos) // Get all todos
  .get("/api/todos/:id", getTodo); // Get one todo
  .put("/api/todos/:id", updateTodo) // Update a todo

export default router;
```

This route will use the `PUT` method.

### Testing the update route

Remember to restart the server. To test this route, we’ll use a combination of the previous tests. 

Our method will be `PUT`. Our URL will be `localhost:3000/api/todos/1`. And we’ll include a json document in our `body` with the updated fields. 

```json
{
  "title": "Todo 1",
  "complete": true
}
```

Our response this time will indicate if a document was found, or matched, and if a modification was made. Here we see that both are true. 

If we run a `GET` request on that same URL we’ll see that the document was updated!

## Delete route

Next, we'll set up our delete route. We’ll call this one `deleteTodo`.

```jsx
const deleteTodo = async ({
  params,
  response,
}: {
  params: { id: string };
  response: any;
}) => {
  try {
    const URI = `${BASE_URI}/deleteOne`;
    const query = {
      collection: COLLECTION,
      database: DATABASE,
      dataSource: DATA_SOURCE,
      filter: { todoId: parseInt(params.id) }
    };
    options.body = JSON.stringify(query);
    const dataResponse = await fetch(URI, options);
    const todoDeleted = await dataResponse.json();

    response.status = 201;
    response.body = {
      todoDeleted
    };
  } catch (err) {
    response.body = {
      success: false,
      msg: err.toString(),
    };
  }
};
```

This route will use the `deleteOne` Data API endpoint and will `filter` using the URL `params`.

Let’s export this function.

```jsx
export { addTodo, getTodos, getTodo, updateTodo, deleteTodo };
```

And we’ll import it and set up its route in the `routes.ts` file.

```jsx
import { Router } from "https://deno.land/x/oak/mod.ts";
import { 
  addTodo, 
  getTodos, 
  getTodo,
  updateTodo,
  deleteTodo
} from "./controllers/todos.ts"; // Import controller methods

const router = new Router();

// Implement routes
router
  .post("/api/todos", addTodo) // Add a todo
  .get("/api/todos", getTodos) // Get all todos
  .get("/api/todos/:id", getTodo); // Get one todo
  .put("/api/todos/:id", updateTodo) // Update a todo
  .delete("/api/todos/:id", deleteTodo); // Delete a todo

export default router;
```

### Testing the delete route

Remember to restart the server. This test will use the `DELETE` method. We’ll delete the first todo using this URL: `localhost:3000/api/todos/1`.

Our response will indicate how many documents were deleted. In this case, we should see that one was deleted. 

## Bonus: Aggregation route

We're going to create one more bonus route. This one will demonstrate a basic aggregation pipeline using the MongoDB Atlas Data API. We'll call this one `getIncompleteTodos`.

```jsx
const getIncompleteTodos = async ({ response }: { response: any }) => {
  const URI = `${BASE_URI}/aggregate`;
  const pipeline = 
    {
      $match: {
        complete: false
      }
    }, 
    {
      $count: 'incomplete'
    }
  ];
  const query = {
    dataSource: DATA_SOURCE,
    database: DATABASE,
    collection: COLLECTION,
    pipeline
  };

  options.body = JSON.stringify(query);
  const dataResponse = await fetch(URI, options);
  const incompleteCount = await dataResponse.json();
  
  if (incompleteCount) {
    response.status = 200;
    response.body = {
      success: true,
      incompleteCount,
    };
  } else {
    response.status = 404;
    response.body = {
      success: false,
      msg: "No incomplete todos found",
    };
  }
};
```

For this route, we'll use the `aggregate` Data API endpoint. This endpoint will accept a `pipeline`.

We can pass any aggregation pipeline through this endpoint. Our example will be basic. The result will be a count of the incomplete todos. 

Let’s export this final function.

```jsx
export { addTodo, getTodos, getTodo, updateTodo, deleteTodo, getIncompleteTodos };
```

And we’ll import it and set up our final route in the `routes.ts` file.

```jsx
import { Router } from "https://deno.land/x/oak/mod.ts";
import { 
  addTodo, 
  getTodos, 
  getTodo,
  updateTodo,
  deleteTodo,
  getIncompleteTodos
} from "./controllers/todos.ts"; // Import controller methods

const router = new Router();

// Implement routes
router
  .post("/api/todos", addTodo) // Add a todo
  .get("/api/todos", getTodos) // Get all todos
  .get("/api/todos/:id", getTodo); // Get one todo
  .get("/api/todos/incomplete/count", getIncompleteTodos) // Get incomplete todo count
  .put("/api/todos/:id", updateTodo) // Update a todo
  .delete("/api/todos/:id", deleteTodo); // Delete a todo

export default router;
```

### Testing the aggregation route

Remember to restart the server. This test will use the `GET` method and this URL: `localhost:3000/api/todos/incomplete/count`.

Add a few test todos to the database and mark some as complete and some as incomplete.

![

Our response shows the count of incomplete todos. 

## Conclusion

We created a Deno server that uses the MongoDB Atlas Data API to Create, Read, Update, and Delete (CRUD) documents in our MongoDB database. We added a bonus route to demonstrate using an aggregation pipeline with the MongoDB Atlas Data API. What next?

If you would like to see the completed code, you can find it *here*. You should be able to use this as a starter for your next project and modify it to meet your needs.

I’d love to hear your feedback or questions. Let’s chat in the MongoDB Community.