Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
JavaScript
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
JavaScriptchevron-right

Getting Started With Deno 2.0 & MongoDB

Jesse Hall13 min read • Published Jan 21, 2022 • Updated Oct 22, 2024
AtlasTypeScriptJavaScript
FULL APPLICATION
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Deno, the "modern" runtime for JavaScript and TypeScript built in Rust, has recently released version 2.0. This major update brings significant improvements and new features that make Deno an even more attractive option for developers.
If you're familiar with Node.js, you'll find Deno quite similar but with some key improvements. From the same creator as Node.js, Ryan Dahl, Deno is designed to be a more secure and modern successor to Node.js.
Fun fact: Deno is an anagram. Rearrange the letters in Node to spell Deno.
Deno now supports package managers like npm and JSR, while still maintaining the ability to import directly from URLs without a package manager. It uses ES modules, has first-class await support, has built-in testing, and implements web standard APIs where possible, such as built-in fetch and localStorage. This flexibility in dependency management allows developers to choose the approach that best suits their project needs.
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.
Learn by watching
Here is a video version of this article if you prefer to watch.

What's new in Deno 2.0?

Deno 2.0 introduces several exciting features and improvements:
  1. Enhanced npm compatibility: Deno now supports a wider range of npm packages, including the official MongoDB driver.
  2. Improved performance: Significant speed improvements in various operations.
  3. Built-in test runner: No need for external testing frameworks.
  4. Native HTTP server: Build simple web applications without third-party frameworks.
  5. Enhanced security model: More granular permissions and improved security features.
In this tutorial, we'll explore some of these new features while building a simple CRUD application using MongoDB.

Prerequisites

  • Basic TypeScript knowledge
  • Understanding of MongoDB concepts
  • Familiarity with RESTful APIs

Setting up Deno 2.0

To get started with Deno 2.0, you'll need to install or update Deno on your system.
  • For macOS and Linux:
    curl -fsSL https://deno.land/install.sh | sh
  • For Windows (using PowerShell):
    irm https://deno.land/install.ps1 | iex
For more options, check the official Deno installation instructions.

Deno VS Code extension

If you are using VS Code, I highly recommend installing the official Denoland extension. This extension enables type checking, IntelliSense, Deno CLI integration, and much more.

Creating a basic HTTP server with routing

With Deno 2.0, we can create a simple HTTP server with routing capabilities using only built-in features. Let's start by creating a server.ts file:
1const PORT = 3000;
2
3async function handler(req: Request): Promise<Response> {
4 const url = new URL(req.url);
5 const path = url.pathname;
6
7 if (req.method === "GET" && path === "/") {
8 return new Response("Hello, World!");
9 } else if (req.method === "POST" && path === "/api/todos") {
10 // Handle POST /api/todos
11 } else if (req.method === "GET" && path === "/api/todos") {
12 // Handle GET /api/todos
13 } else if (req.method === "GET" && path === "/api/todos/incomplete/count") {
14 // Handle GET /api/todos/incomplete/count
15 } else if (req.method === "GET" && path.startsWith("/api/todos/")) {
16 // Handle GET /api/todos/:id
17 } else if (req.method === "PUT" && path.startsWith("/api/todos/")) {
18 // Handle PUT /api/todos/:id
19 } else if (req.method === "DELETE" && path.startsWith("/api/todos/")) {
20 // Handle DELETE /api/todos/:id
21 } else {
22 return new Response("Not Found", { status: 404 });
23 }
24}
25
26console.log(`HTTP webserver running. Access it at: http://localhost:${PORT}/`);
27Deno.serve({ port: PORT }, handler);
This sets up a basic HTTP server using Deno's built-in Deno.serve function. The handler function processes incoming requests, routing them based on the HTTP method and URL path. It includes placeholders for handling various CRUD operations on a "todos" resource, as well as a special route for counting incomplete todos. If no matching route is found, it returns a 404 "Not Found" response. The server listens on port 3000, and a console message is logged to indicate that the server is running.
Now, we can start our Deno server by running the following command:
1deno run --allow-env --allow-read --allow-net --allow-sys --env --watch server.ts
This will start our server and watch for changes to our code. If you make changes, the server will automatically restart. Remember, Deno is very secure by default, so we need to use the appropriate --allow-* flags to allow our server to function.
Note: With Deno 2.0, you can now use shorthand flags to allow the necessary permissions.
1deno -ERNS --env --watch server.ts
The -E flag allows environment variables, R allows reading files, N allows network access, S allows system access, and --env allows the use of environment variables.

Testing our server

Now, we can test our server by navigating to http://localhost:3000 in our web browser. We should see "Hello, World!" displayed in the browser.

Setting up MongoDB connection

Now that we have a basic server, let's set up our MongoDB connection. We'll use the official MongoDB npm package, which is now fully compatible with Deno 2.0.
First, let's create a new file called db.ts:
1import { MongoClient } from "npm:mongodb@5.6.0";
2
3const MONGODB_URI = Deno.env.get("MONGODB_URI") || "";
4const DB_NAME = Deno.env.get("DB_NAME") || "todo_db";
5
6if (!MONGODB_URI) {
7 console.error("MONGODB_URI is not set");
8 Deno.exit(1);
9}
10
11const client = new MongoClient(MONGODB_URI);
12
13try {
14 await client.connect();
15 await client.db("admin").command({ ping: 1 });
16 console.log("Connected to MongoDB");
17} catch (error) {
18 console.error("Error connecting to MongoDB:", error);
19 Deno.exit(1);
20}
21
22const db = client.db(DB_NAME);
23const todos = db.collection("todos");
24
25export { db, todos };
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.
With Deno 2.0, you can opt to use a package manager like npm or JSR. You can also use an existing Node.js project with Deno and it will utilize the package.json file.
In this file, we import the MongoClient from the official MongoDB npm package and create a new instance of it. We then connect to our MongoDB instance using the MONGODB_URI environment variable. If the variable is not set, we log an error and exit the process.
Once connected, we ping the database to ensure that our connection is working. If the ping fails, we log an error and exit the process.
We define our database and collection and export them so that we can use them in our application.
Before we can use this file, we'll need to set our MONGODB_URI and DB_NAME environment variables. We can do this by creating a .env file and adding the following:
1MONGODB_URI="...your connection string..."
2DB_NAME="todo_db"
The easiest way to get your connection string is to use the MongoDB Atlas GUI. If you don't already have an account, sign up for a free-forever tier. Check out the Connect to Your Cluster documentation for more information on how to get your connection string.

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 todoController.ts.
In the todoController.ts file, we'll first import our todos collection and the ObjectId from the mongodb npm package:
1import { todos } from "../db.ts";
2import { ObjectId } from "npm:mongodb@5.6.0";

Create route

Now, we can start creating our first route function. We'll call this function addTodo. This function will add a new todo item to our database collection.
1// ... imports
2
3async function addTodo(req: Request): Promise<Response> {
4 try {
5 const body = await req.json();
6 const result = await todos.insertOne(body);
7 return new Response(JSON.stringify({ id: result.insertedId }), {
8 status: 201,
9 headers: { "Content-Type": "application/json" },
10 });
11 } catch (error) {
12 return new Response(JSON.stringify({ error: error.message }), {
13 status: 400,
14 headers: { "Content-Type": "application/json" },
15 });
16 }
17}
18
19export { addTodo };
The addTodo function takes a Request object, extracts and parses its JSON body, and attempts to insert this data into the todos collection. On success, it returns a Response with a 201 status code and the new document's ID. If an error occurs, it returns a 400 status code with the error message. The function uses try-catch for error handling and returns JSON responses, adhering to RESTful API practices.
Let's add the addTodo function to our handler function in the server.ts file.
1import { addTodo } from "./controllers/todoController.ts";
2// ... existing code
3
4async function handler(req: Request): Promise<Response> {
5 // ... existing code
6 else if (req.method === "POST" && path === "/api/todos") {
7 return await addTodo(req);
8 }
9 // ... existing code
10}

Testing the create route

Now, we can test our addTodo function. We'll use the curl command to send a POST request to our create route. Alternatively, you can use a tool like Postman, Insomnia, or Thunder Client in VS Code to send the request.
1curl -X POST http://localhost:3000/api/todos -H "Content-Type: application/json" -d '{"title": "Todo 1", "complete": false}'
You should see a response with a 201 status code and the ID of the new todo.

Read all documents route

Let's move on to the read routes. We'll start with a route that gets all of our todos, called getTodos. This will go into the todoController.ts file.
1// ... existing code
2
3async function getTodos(): Promise<Response> {
4 try {
5 const allTodos = await todos.find().toArray();
6 return new Response(JSON.stringify(allTodos), {
7 headers: { "Content-Type": "application/json" },
8 });
9 } catch (error) {
10 return new Response(JSON.stringify({ error: error.message }), {
11 status: 500,
12 headers: { "Content-Type": "application/json" },
13 });
14 }
15}
16
17export { addTodo, getTodos };
This function retrieves all todo items from the todos collection using the find MongoDB method. If successful, it returns a JSON response containing all the todos with a 200 status code. If an error occurs during the process, it catches the error and returns a JSON response with the error message and a 500 status code. Finally, the function is exported.
Let's add this to our handler function in the server.ts file.
1import { addTodo, getTodos } from "./controllers/todoController.ts";
2// ... existing code
3
4async function handler(req: Request): Promise<Response> {
5 // ... existing code
6 else if (req.method === "GET" && path === "/api/todos") {
7 return await getTodos();
8 }
9 // ... existing code
10}

Testing the read all route

Now, we can test our getTodos function. We'll use the curl command to send a GET request to our read route.
1curl http://localhost:3000/api/todos
You should see a response with a 200 status code and a JSON array of todos. Note the id in the response. We'll need this for our next route.

Read a single document route

Next, we'll set up our function to read a single document. We'll call this one getTodo and again put this in the todoController.ts file.
1// ... existing code
2
3async function getTodo(id: string): Promise<Response> {
4 try {
5 const todo = await todos.findOne({ _id: new ObjectId(id) });
6 if (!todo) {
7 return new Response(JSON.stringify({ error: "Todo not found" }), {
8 status: 404,
9 headers: { "Content-Type": "application/json" },
10 });
11 }
12 return new Response(JSON.stringify(todo), {
13 headers: { "Content-Type": "application/json" },
14 });
15 } catch (error) {
16 return new Response(JSON.stringify({ error: error.message }), {
17 status: 500,
18 headers: { "Content-Type": "application/json" },
19 });
20 }
21}
22
23export { addTodo, getTodos, getTodo };
This function retrieves a single todo item from the todos collection using the findOne MongoDB method based on the provided id. If the todo item is found, it returns a JSON response with the todo data and a 200 status code. If the todo item is not found, it returns a 404 status code with an error message. If an error occurs during the process, it catches the error and returns a JSON response with the error message and a 500 status code. Finally, the function is exported.
Let's add this to our handler function in the server.ts file.
1import { addTodo, getTodos, getTodo } from "./controllers/todoController.ts";
2// ... existing code
3
4async function handler(req: Request): Promise<Response> {
5 // ... existing code
6 else if (req.method === "GET" && path.startsWith("/api/todos/")) {
7 const id = path.split("/")[3];
8 return await getTodo(id);
9 }
10 // ... existing code
11}
In this route, we need to extract the id from the URL path and pass that to our getTodo function. We can do this by splitting the path at the / and taking the fourth element (index 3).

Testing the read single route

Now, we can test our getTodo function. We'll use the curl command to send a GET request to our read single route. Remember that _id we got from the previous test? We'll need to use that here.
1curl http://localhost:3000/api/todos/<...id here...>
2# example:
3# curl http://localhost:3000/api/todos/67005eef3bf67a631efc95f6
You should see a response with a 200 status code and a JSON object representing the todo.

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.
1// ... existing code
2
3async function updateTodo(id: string, req: Request): Promise<Response> {
4 try {
5 const body = await req.json();
6 const result = await todos.updateOne(
7 { _id: new ObjectId(id) },
8 { $set: body },
9 );
10 if (result.matchedCount === 0) {
11 return new Response(JSON.stringify({ error: "Todo not found" }), {
12 status: 404,
13 headers: { "Content-Type": "application/json" },
14 });
15 }
16 return new Response(JSON.stringify({ updated: result.modifiedCount }), {
17 headers: { "Content-Type": "application/json" },
18 });
19 } catch (error) {
20 return new Response(JSON.stringify({ error: error.message }), {
21 status: 400,
22 headers: { "Content-Type": "application/json" },
23 });
24 }
25}
26
27export { addTodo, getTodos, getTodo, updateTodo };
This function updates a todo item in the todos collection based on the provided id. It extracts the updated data from the request body, uses the updateOne MongoDBmethod to modify the existing document, and returns a JSON response indicating the number of documents modified. If the todo item is not found, it returns a 404 status code with an error message. If an error occurs during the process, it catches the error and returns a JSON response with the error message and a 400 status code. Finally, the function is exported.
Let's add this to our handler function in the server.ts file.
1import { addTodo, getTodos, getTodo, updateTodo } from "./controllers/todoController.ts";
2// ... existing code
3
4async function handler(req: Request): Promise<Response> {
5 // ... existing code
6 else if (req.method === "PUT" && path.startsWith("/api/todos/")) {
7 const id = path.split("/")[3];
8 return await updateTodo(id, req);
9 }
10 // ... existing code
11}
This time, we'll pass both the id and the request to our updateTodo function.

Testing the update route

Now, we can test our updateTodo function. We'll use the curl command to send a PUT request to our update route. You can use the _id from the previous tests.
1curl -X PUT http://localhost:3000/api/todos/<...id here...> -H "Content-Type: application/json" -d '{"title": "Updated Todo", "complete": true}'
You should see a response with a 200 status code and a JSON object indicating the number of documents modified.

Delete route

Next, we'll set up our delete route. We'll call this one deleteTodo.
1// ... existing code
2
3async function deleteTodo(id: string): Promise<Response> {
4 try {
5 const result = await todos.deleteOne({ _id: new ObjectId(id) });
6 if (result.deletedCount === 0) {
7 return new Response(JSON.stringify({ error: "Todo not found" }), {
8 status: 404,
9 headers: { "Content-Type": "application/json" },
10 });
11 }
12 return new Response(JSON.stringify({ deleted: result.deletedCount }), {
13 status: 200,
14 headers: { "Content-Type": "application/json" },
15 });
16 } catch (error) {
17 return new Response(JSON.stringify({ error: error.message }), {
18 status: 400,
19 headers: { "Content-Type": "application/json" },
20 });
21 }
22}
23
24export { addTodo, getTodos, getTodo, updateTodo, deleteTodo };
This function deletes a todo item from the todos collection based on the provided id. It uses the deleteOne MongoDB method to remove the document and returns a 200 status code on success along with a JSON response indicating the number of documents deleted. If the todo item is not found, it returns a 404 status code with an error message. If an error occurs during the process, it catches the error and returns a JSON response with the error message and a 400 status code. Finally, the function is exported.
Let's add this to our handler function in the server.ts file.
1import { addTodo, getTodos, getTodo, updateTodo, deleteTodo } from "./controllers/todoController.ts";
2// ... existing code
3
4async function handler(req: Request): Promise<Response> {
5 // ... existing code
6 else if (req.method === "DELETE" && path.startsWith("/api/todos/")) {
7 const id = path.split("/")[3];
8 return await deleteTodo(id);
9 }
10 // ... existing code
11}
This time, we'll pass the id to our deleteTodo function.

Testing the delete route

Now, we can test our deleteTodo function. We'll use the curl command to send a DELETE request to our delete route.
1curl -X DELETE http://localhost:3000/api/todos/<...id here...>
You should see a response with a 204 status code.

Bonus: Aggregation route

We're going to create one more bonus route. This one will demonstrate a basic aggregation pipeline. We'll call this one getIncompleteTodos.
1// ... existing code
2
3async function getIncompleteTodos(): Promise<Response> {
4 try {
5 const pipeline = [
6 { $match: { complete: false } },
7 { $count: "incomplete" },
8 ];
9 const result = await todos.aggregate(pipeline).toArray();
10 const incompleteCount = result[0]?.incomplete || 0;
11 return new Response(JSON.stringify({ incompleteCount }), {
12 headers: { "Content-Type": "application/json" },
13 });
14 } catch (error) {
15 return new Response(JSON.stringify({ error: error.message }), {
16 status: 500,
17 headers: { "Content-Type": "application/json" },
18 });
19 }
20}
21
22export { addTodo, deleteTodo, getIncompleteTodos, getTodo, getTodos, updateTodo };
This function performs an aggregation pipeline on the todos collection to count the number of incomplete todos. It uses the $match stage to filter todos where complete is false and the $count stage to count the number of documents that match this criteria. The result is returned as a JSON response with a 200 status code. If an error occurs during the process, it catches the error and returns a JSON response with the error message and a 500 status code.
Let's add this to our handler function in the server.ts file.
1import { addTodo, deleteTodo, getTodo, getTodos, updateTodo, getIncompleteTodos } from "./controllers/todoController.ts";
2// ... existing code
3
4async function handler(req: Request): Promise<Response> {
5 // ... existing code
6 else if (req.method === "GET" && path === "/api/todos/incomplete/count") {
7 return await getIncompleteTodos();
8 }
9 // ... existing code
10}
Note that this route is placed above the getTodo route in the handler function. This is because the getTodo route uses a path that matches the beginning of the getIncompleteTodos route. If we placed getIncompleteTodos below getTodo in the handler function, the server would not be able to distinguish between the two routes and would always match the getTodo route.
Alternatively, we could use a regular expression to match the path and place getIncompleteTodos below getTodo in the handler function. This would allow us to use a more specific path for our aggregation route.

Testing the aggregation route

Now, we can test our getIncompleteTodos function. We'll use the curl command to send a GET request to our aggregation route.
1curl http://localhost:3000/api/todos/incomplete/count
You should see a response with a 200 status code and a JSON object containing the count of incomplete todos.

Conclusion

In this tutorial, we created a Deno server that uses the MongoDB driver 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 driver. What next?
The complete code can be found in the Getting Started With Deno & MongoDB repository. 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.

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Tutorial

Real-Time Chat in a Phaser Game with MongoDB and Socket.io


Feb 03, 2023 | 11 min read
Quickstart

Getting Started With MongoDB & Mongoose


Aug 05, 2024 | 9 min read
Tutorial

Real Time Data in a React JavaScript Front-End with Change Streams


Sep 09, 2024 | 6 min read
Article

How to Enable Local and Automatic Testing of Atlas Search-Based Features


Jun 12, 2024 | 8 min read
Table of Contents