DEV Community

MongoDB Guests for MongoDB

Posted on

Build Your AI-Powered Reading List Using DigitalOcean Functions, Mistral AI, and MongoDB

This Tutorial was written by Diego Freniche.

Since I started browsing the Internet, I've used many different services, applications, and browser plugins (even the good old bookmarks) to mark articles and posts that seemed interesting and that I wanted to read. Only not now. I'll read them later, I promise. And then you hoard a bunch of links in one place. But when you go back to actually read them, you find yourself clicking again on them just to remember what those links where about and why did you store them in the first place...

Fast forward today, and I haven't changed much in how I interact with articles I want to read later. But nowadays we can use LLMs almost for free... and they are really good at writing text summaries. So, why not create a system that, given a link, generates a summary and then stores both the link and the summary in a fast database I can use to build my own UI later?

And this is now yet another read-it-later (maybe) system was born...

Prerequisites

Before you begin this guide, you'll need the following:

  • a DigitalOcean account, that we'll use to write a DigitalOcean Function. This function will receive our links and run the code that will generate the LLM-powered summary and store the results in a database.
  • a MongoDB database, also running in DigitalOcean. We will use it to store the links and summaries of the web pages. You can learn more about deploying a managed MongoDB instance in DigitalOcean here.
  • access to any LLM that can write a good summary. For this example, I'll use Mistral AI, so you'll need to register a free account there and get an API token. Take a note of that API key, we'll need it later!

Step 1 - Creating an Empty DigitalOcean Function

Let's start by creating the backbone of our system. We can write a backend app using Node, Spring Boot or any other web framework. But then we need to host it in a server, write the code, correctly size the server, update the operating system and dependencies, etc. Using a serverless function we focus on the task at hand. In this case, getting a URL via a parameter, producing a summary of the page, and storing the data in the database.

So head over to Functions and click on create > Functions. First, we need to choose where our serverless function will be hosted. Although it's a serverless function, our code will need a real server to run. Only we don't have to provide it. Choose a data center close to your location (or close to where the users of your function will be). Then give it a name, choosing a label. In my case, it will be read-it-later-i-pinly-promise.

After creating the namespace, you'll be in the Overview tab. Let's add some basic code to test our function! Just click on the Create Function button.

We need to select one of the supported runtimes (in our case, Nodejs 18), and we'll give the function a name: read_later. It's important to leave the Web Function box checked, as we want to call this function directly using HTTP.

Done! Now you should see the default source code of the newly created function. This just gets an argument name from the URL and returns it. As we're not configuring a web page or a JSON response, this will be printed out as plain text by our web browser.

function main(args) {
let name = args.name || 'stranger' let greeting = 'Hello ' + name + '!' console.log(greeting)
return {"body": greeting}
}
Enter fullscreen mode Exit fullscreen mode

In the main function, we receive a JSON object with all the arguments passed in the function call, we search for name, and use it. If not found, we use stranger as name. Then we compose and print out a greeting which we also return from our function.

Step 2 - testing our function

As this is accessible via HTTP calls, we have several different options to test our function.

Using cURL

If your system includes curl (Microsoft Windows 10/11 and most UNIX systems, like Linux and macOS come with cURL preinstalled), you can copy the URL of your function and add the name parameter at the end. Then call curl with:

curl https://<redacted>.doserverless.co/api/v1/web/<redacted>/default/read_later? name=Diego
Enter fullscreen mode Exit fullscreen mode

In your case, instead of <redacted>, you'll see the ids of your functions. If you call the function from a Terminal, you'll see the response from our function:

curl https://<redacted>.doserverless.co/api/v1/web/<redacted>/default/read_later? name=Diego
Hello Diego!%
Enter fullscreen mode Exit fullscreen mode

Try it changing the name and the code in your function and experiment with it! It's super fun!

Testing directly in DigitalOcean

We can click on Test Parameters and add a JSON with our test data, in this case:

{ "name": "Diego" }
Enter fullscreen mode Exit fullscreen mode

Then, we can hit the Run button and see the results in the Output and Logs windows.

Step 3 - Adding the LLM Code

First, we will change our variable names to reflect what we're doing: producing summaries from links. Instead of name we'll use a link variable. Also, we'll return a proper HTTP response, with
statusCode 200 in case we got a link.

export async function main(args) { const link = args.link;
if (!link) { return {
statusCode: 400,
headers: { "Content-Type": "application/json" }, body: { error: "Please send a link" }
};
}
const body = { URL: link }; return {
statusCode: 200,
headers: { "Content-Type": "application/json" }, body: body
};
}
Enter fullscreen mode Exit fullscreen mode

After changing the code, hit the "Save" button to avoid losing your changes. You can now re-run your function and check the results.

While we're at it, let's also change the Test Parameters (if you're testing directly from the browser) to:

(
"link": "https://en.wikipedia.org/wiki/Seville"
)
Enter fullscreen mode Exit fullscreen mode

Seems like we're about to travel to Seville!

After that, we'll add the code to interact with Mistral. We need to send a POST with the prompt to their endpoint. For this, we'll need an API key from Mistral AI (see prerequisites). Save the code, but don't run the function (yet) as we need to add the secret.

const API_KEY = process.env.MISTRAL_API_KEY; // secrets are stored in the function's Settings
const API_URL = 'https://api.mistral.ai/v1/chat/completions';

async function chatWithLeChat(prompt) { try {
// call the Mistral endpoint with POST const res = await fetch(API_URL, {
method: 'POST', headers: {
'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'mistral-tiny',
messages: [{ role: 'user', content: prompt }],
}),
});

const data = await res.json(); console.log(res.status, data);
return data?.choices?.[0]?.message?.content || null;
} catch (error) {
console.error('Error:', error?.message || error); return null;
}

Enter fullscreen mode Exit fullscreen mode

Step 4 - Adding Secrets

As you can see, in our code, we don't embed the secrets (like API keys). Instead, we use the Functions environment to provide these:

const API_KEY = process.env.MISTRAL_API_KEY;
Enter fullscreen mode Exit fullscreen mode

To add this secret MISTRAL_API_KEY we click on Settings. Then, we scroll down to Environment Variables. There, we can click on Edit

Now, click on Add and then Save. You should see your new environment variable now!

Step 5 - getting the LLM summary

It's time to call our chatWithLeChat function from main! To do that, we 1st create a system prompt that includes the link we received and instructs the LLM on what to do. Then, we'll call Mistral:

const prompt = 'I need a summary of this page: ' + link;

const summary = await chatWithLeChat(prompt);
Enter fullscreen mode Exit fullscreen mode

We will extend our body to include the summary:

const body = { URL: link , summary: summary
};
Enter fullscreen mode Exit fullscreen mode

This is all our code right now:

export async function main(args) { const link = args.link;
if (!link) { return {
statusCode: 400,
headers: { "Content-Type": "application/json" }, body: { error: "Please send a link" }
};
}

const prompt = 'I need a summary of this page: ' + link; const summary = await chatWithLeChat(prompt);
const body = { URL: link , summary: summary
};

return { statusCode: 200,
headers: { "Content-Type": "application/json" }, body: body
};
}

const API_KEY = process.env.MISTRAL_API_KEY; // secrets are stored in the function's Settings
const API_URL = 'https://api.mistral.ai/v1/chat/completions';

async function chatWithLeChat(prompt) { try {
// call the Mistral endpoint with POST const res = await fetch(API_URL, {
method: 'POST', headers: {
'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'mistral-tiny',
messages: [{ role: 'user', content: prompt }],
}),
});

const data = await res.json(); console.log(res.status, data);
return data?.choices?.[0]?.message?.content || null;
} catch (error) {
console.error('Error:', error?.message || error); return null;
}
Enter fullscreen mode Exit fullscreen mode

If you save your function and run it, most probably you'll get an error message in LOGS:

2026-02-02T12:14:17.299547912Z stderr: The function exceeded its time limits of 3000 milliseconds. Logs might be missing.
Enter fullscreen mode Exit fullscreen mode

We need to give our function more time to wait for the LLM to answer!

Setting Timeouts

Head to Settings again, and click Edit on Limits. By default you have 3 secs (3000 milliseconds) to run your function before you get a timeout. We'll set that to 10 secs. Save the change, go back to Source and run the function again.

After that change, you should get a summary of the page!

Persisting data into MongoDB

We're now ready to store our body object in a MongoDB collection. One of the nicest things about MongoDB is that you work with your application objects in memory, and when ready, you send them to MongoDB using the driver (a library that will interact directly with the database). No need for ORMs or complicated frameworks in this case.

We'll start by importing the MongoDB driver:

import { MongoClient } from "mongodb";
Enter fullscreen mode Exit fullscreen mode

After that, we'll write a simple function that will:

  • receive our object as an argument, named page.
  • connect to MongoDB using a MongoClient instance and the connect method.
  • write it into MongoDB using insertOne
async function store_in_mongodb(page) {
const mongodb_URI = process.env.MONGODB_URI; // get the MongoDB URI from an environment variable

const client = new MongoClient(mongodb_URI); try {
// connect to MongoDB await client.connect();
const database = client.db('read_it_later'); const collection = database.collection('pages');

const result = await collection.insertOne(page); return result;
} finally {
await client.close();
}
};
Enter fullscreen mode Exit fullscreen mode

As we did previously with our environment variable MISTRAL_API_KEY, we'll need to add a new environment variable in Settings called MONGODB_URI with the URI of our MongoDB installation. The URI looks like this:

mongodb+srv://user:password@cluster.something-here.mongodb.net/
Enter fullscreen mode Exit fullscreen mode

The last step will be to call this new function in main:

await store_in_mongodb(greeting);
Enter fullscreen mode Exit fullscreen mode

Once this is done, save your function and send some links to your function and see how the collection pages gets populated!

Conclusion

In this article we've explored how to write a serverless function that can interact with an LLM and store results in MongoDB. It's incredible how far a few lines of code can take us these days. Not so long ago you needed to spend days setting up web and database servers, hosting everything, exposing it to the Internet, writing your code, etc. Now, using DigitalOcean integrated suit of services and hosted applications you focus on what matters most: your application code. Our next step could be building a new function to read from MongoDB, that will be the base for our UI, or add Semantic Search using MongoDB's Vector Search capabilities.

Here you have the complete code listing for our function:

export async function main(args) { const link = args.link;
if (!link) { return {
statusCode: 400,
headers: { "Content-Type": "application/json" }, body: { error: "Please send a link" }
};
}

const prompt = 'I need a summary of this page: ' + link; const summary = await chatWithLeChat(prompt);
const body = { URL: link , summary: summary
};

await store_in_mongodb(body);

return { statusCode: 200,
headers: { "Content-Type": "application/json" }, body: body
};
}

const API_KEY = process.env.MISTRAL_API_KEY; // secrets are stored in the function's Settings
const API_URL = 'https://api.mistral.ai/v1/chat/completions';

async function chatWithLeChat(prompt) { try {
// call the Mistral endpoint with POST const res = await fetch(API_URL, {
method: 'POST', headers: {
'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'mistral-tiny',
messages: [{ role: 'user', content: prompt }],
}),
});

const data = await res.json(); console.log(res.status, data);
return data?.choices?.[0]?.message?.content || null;
} catch (error) {
console.error('Error:', error?.message || error); return null;
}
}
import { MongoClient } from "mongodb"; async function store_in_mongodb(page) {
const mongodb_URI = process.env.MONGODB_URI; // get the MongoDB URI from an environment variable

const client = new MongoClient(mongodb_URI); try {
// connect to MongoDB await client.connect();
const database = client.db('read_it_later'); const collection = database.collection('pages');

const result = await collection.insertOne(page); return result;
} finally {
await client.close();
}
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)