HomeLearnArticleNext Gen Web Apps with Remix and MongoDB Atlas Data API

Next Gen Web Apps with Remix and MongoDB Atlas Data API

Updated: Apr 01, 2022 |

Published: Jan 26, 2022

  • Atlas
  • Atlas Search
  • JavaScript
  • ...

By Pavel Duchovny

 and Stanimira Vlaeva

Rate this article

Javascript-based application stacks have proven themselves to be the dominating architecture for web applications we all use. From MEAN to MERN and MEVN, the idea is to have a JavaScript-based web client and server, communicating through a REST or GraphQL API powered by the document model of MongoDB as its flexible data store.

Remix is a new JS framework that comes to disrupt the perception of static websites and tiering the view and the controller. This framework aims to simplify the web component nesting by turning our web components into small microservices that can load, manipulate, and present data based on the specific use case and application state.

The idea of combining the view logic with the server business logic and data load, leaving the state management and binding to the framework, makes the development fast and agile. Now, adding a data access layer such as MongoDB Atlas and its new Data API makes building data-driven web applications super simple. No driver is needed and everything happens in a loader function via some https calls.

To showcase how easy it is, I have built a demo movie search application based on MongoDB Atlas sample database sample_mflix. In this article, we will cover the main features of this application and learn how to use Atlas Data API and Atlas Search features.

If you want to get familiar with Remix in general, I recommend reading its documentation and exploring its tutorials.

Movie App in action
The Movie Search application in action

#Setting Up an Atlas Cluster and Data API

First we need to prepare our data tier that we will work with our Remix application. Follow these steps:

Atlas Cluster

Data API Configuration

#Setting Up Remix Application

As other Node frameworks, the easiest way to bootstrap an app is by deploying a template application as a base:

1npx create-remix@latest

The command will prompt for several settings. I have used the default ones with the default self hosting option.

Let’s also add a few node packages that we’ll be using in our application. Navigate to your newly created project and execute the following command:

1npm install axios dotenv tiny-invariant

The application consists of two main files which host the entry point to the demo application with main page html components: app/root.jsx and app/routes/index.jsx. In the real world, it will probably be the routing to a login or main page.

1- app
2 - routes
3 - index.jsx
4 - root.jsx

In app/root.jsx, we have the main building blocks of creating our main page and menu to route us to the different demos.

1<nav aria-label="Main navigation" className="remix-app__header-nav">
2 <ul>
3 <li>
4 <Link to="/">Home</Link>
5 </li>
6 <li>
7 <Link to="/movies">Movies Search Demo</Link>
8 </li>
9 <li>
10 <Link to="/facets">Facet Search Demo</Link>
11 </li>
12 <li>
13 <a href="https://github.com/mongodb-developer/atlas-data-api-remix">GitHub</a>
14 </li>
15 </ul>
16</nav>

If you choose to use TypeScript while creating the application, add the navigation menu to app/routes/index.tsx instead. Don't forget to import Link from remix.

Main areas are exported in the app/routes/index.jsx under the “routes” directory which we will introduce in the following section.

This file uses the same logic of a UI representation returned as JSX while loading of data is happening in the loader function. In this case, the loader only provides some static data from the “data” variable.

Now, here is where Remix introduces the clever routing in the form of routes directories named after our URL path conventions. For the main demo called “movies,” we created a “movies” route:

1- routes
2 - movies
3 - $title.jsx
4 - index.jsx

The idea is that whenever our application is redirecting to <BASE_URL>/movies, the index.jsx under routes/movies is called. Each jsx file produces a React component and loads its data via a loader function (operating as the server backend data provider).

Before we can create our main movies page and fetch the movies from the Atlas Data API, let’s create a .env file in the main directory to provide the needed Atlas information for our application:

1DATA_API_KEY=<API-KEY>
2DATA_API_BASE_URL=<YOUR-DATA-ENDPOINT-URL>
3CLUSTER_NAME=<YOUR-ATLAS-CLUSTER-NANE>

Place the relevant information from your Atlas project locating the API key, the Data API base URL, and the cluster name. Those will be shortly used in our Data API calls.

⚠️Important: .env file is good for development purposes. However, for production environments, consider the appropriate secret repository to store this information for your deployment.

Let’s load this .env file when the application starts by adjusting the “dev” npm scripts in the package.json file:

1"dev": "node -r dotenv/config node_modules/.bin/remix dev"

#movies/index.jsx File

Let's start to create our movies list by rendering it from our data loader and the sample_mflix.movies collection structure.

Navigate to the ‘app/routes’ directory and execute the following commands to create new routes for our movies list and movie details pages.

1cd app/routes
2mkdir movies
3touch movies/index.jsx movies/\$title.jsx

Then, open the movies/index.jsx file in your favorite code editor and add the following:

1import { Form, Link, useLoaderData , useSearchParams, useSubmit } from "remix";
2const axios = require("axios");
3
4export default function Movies() {
5 let [searchParams, setSearchParams] = useSearchParams();
6 let submit = useSubmit();
7 let movies = useLoaderData();
8 let totalFound = movies.totalCount;
9 let totalShow = movies.showCount;
10
11 return (
12 <div> <h1>Movies</h1> <Form method="get"> <input onChange={e => submit(e.currentTarget.form)} id="searchBar" name="searchTerm" placeholder="Search movies..." /> <p>Showing {totalShow} of total {totalFound} movies found</p> </Form> <ul> {movies.documents.map(movie => ( <li key={movie.title}> <Link to={movie.title}>{movie.title}</Link> </li> ))} </ul> </div>
13 );
14}

As you can see in the return clause, we have a title named “Movies,” an input inside a “get” form to post a search input if requested. We will shortly explain how forms are convenient when working with Remix. Additionally, there is a link list of the retrieved movies documents. Using the <Link> component from Remix allows us to create links to each individual movie name. This will allow us to pass the title as a path parameter and trigger the $title.jsx component, which we will build shortly.

The data is retrieved using useLoaderData() which is a helper function provided by the framework to retrieve data from the server-side “loader” function.

#The Loader Function

The interesting part is the loader() function. Let's create one to first retrieve the first 100 movie documents and leave the search for later.

Add the following code to the movies/index.jsx file.

1export let loader = async ({ request }) => {
2 let pipeline = [{ $limit: 100 }];
3
4 let data = JSON.stringify({
5 collection: "movies",
6 database: "sample_mflix",
7 dataSource: process.env.CLUSTER_NAME,
8 pipeline
9 });
10
11 let config = {
12 method: 'post',
13 url: `${process.env.DATA_API_BASE_URL}/action/aggregate`,
14 headers: {
15 'Content-Type': 'application/json',
16 'Access-Control-Request-Headers': '*',
17 'api-key': process.env.DATA_API_KEY
18 },
19 data
20 };
21
22 let movies = await axios(config);
23 let totalFound = await getCountMovies();
24
25 return {
26 showCount: movies?.data?.documents?.length,
27 totalCount: totalFound,
28 documents: movies?.data?.documents
29 };
30};
31
32const getCountMovies = async (countFilter) => {
33 let pipeline = countFilter ?
34 [{ $match: countFilter }, { $count: 'count' }] :
35 [{ $count: 'count' }];
36
37 let data = JSON.stringify({
38 collection: "movies",
39 database: "sample_mflix",
40 dataSource: process.env.CLUSTER_NAME,
41 pipeline
42 });
43
44 let config = {
45 method: 'post',
46 url: `${process.env.DATA_API_BASE_URL}/action/aggregate`,
47 headers: {
48 'Content-Type': 'application/json',
49 'Access-Control-Request-Headers': '*',
50 'api-key': process.env.DATA_API_KEY
51 },
52 data
53 };
54
55 let result = await axios(config);
56
57 return result?.data?.documents[0]?.count;
58}

Here we start with an aggregation pipeline to just limit the first 100 documents for our initial view pipeline = [ {$limit : 100}]; . This pipeline will be passed to our REST API call to the Data API endpoint:

1let data = JSON.stringify({
2 collection: "movies",
3 database: "sample_mflix",
4 dataSource: process.env.CLUSTER_NAME,
5 pipeline
6});
7
8let config = {
9 method: 'post',
10 url: `${process.env.DATA_API_BASE_URL}/action/aggregate`,
11 headers: {
12 'Content-Type': 'application/json',
13 'Access-Control-Request-Headers': '*',
14 'api-key': process.env.DATA_API_KEY
15 },
16 data
17};
18
19let result = await axios(config);

We place the API key and the URL from the secrets file we created earlier as environment variables. The results array will be returned to the UI function:

1return result?.data?.documents[0]?.count;

To run the application, we can go into the main folder and execute the following command:

1npm run dev

The application should start on http://localhost:3000 URL.

For the full text search capabilities of this demo, you need to create a dynamic Atlas Search index on database sample_mflix collection movies (use default dynamic mappings). Require version 4.4.11+ (free tier included) or 5.0.4+ of the Atlas cluster for the search metadata and facet searches we will discuss later. Search Index

Since we have a <Form> Remix component submitting the form input, data typed into the input box will trigger a data reload. The <Form> reloads the loader function without refreshing the entire page. This will naturally resubmit the URL as /movies?searchTerm=<TYPED_VALUE> and here is why it's easy to use the same loader function, extract to URL parameter, and add a search logic by just amending the base pipeline:

1let url = new URL(request.url);
2let searchTerm = url.searchParams.get("searchTerm");
3
4const pipeline = searchTerm ?
5 [
6 {
7 $search: {
8 index: 'default',
9 text: {
10 query: searchTerm,
11 path: {
12 'wildcard': '*'
13 }
14 }
15 }
16 }, { $limit: 100 }, { "$addFields": { meta: "$$SEARCH_META" } }
17 ] :
18 [{ $limit: 100 }];

In this case, the submission of a form will call the loader function again. If there was a searchTermsubmitted in the URL, it will be extracted under the searchTerm variable and create a $search pipeline to interact with the Atlas Search text index.

1text: {
2 query: searchTerm,
3 path: {
4 'wildcard': '*'
5 }
6}

Additionally, there is a very neat feature that allow us to get the metadata for our search—for example, how many matches were for this specific keyword (as we don’t want to show more than 100 results).

1{ "$addFields" : {meta : "$$SEARCH_META"}}

When wiring everything together, we get a working searching functionality, including metadata information on our searches.

Now, if you noticed, each movie title is actually a link redirecting to ./movies/<TITLE> url. But why is this good, you ask? Remix allows us to build parameterized routes based on our URL path parameters.

#movies/$title.jsx File

The movies/$title.jsx file will show each movie's details when loaded. The magic is that the loader function will get the name of the movie from the URL. So, in case we clicked on “Home Alone,” the path will be http:/localhost:3000/movies/Home+Alone.

This will allow us to fetch the specific information for that title.

Open the movies/$title.jsx file we created earlier, and add the following:

1import { Link, useLoaderData } from "remix";
2import invariant from "tiny-invariant";
3
4const axios = require('axios');
5
6export let loader = async ({ params }) => {
7 invariant(params.title, "expected params.title");
8
9 let data = JSON.stringify({
10 collection: "movies",
11 database: "sample_mflix",
12 dataSource: process.env.CLUSTER_NAME,
13 filter: { title: params.title }
14 });
15
16 let config = {
17 method: 'post',
18 url: process.env.DATA_API_BASE_URL + '/action/findOne',
19 headers: {
20 'Content-Type': 'application/json',
21 'Access-Control-Request-Headers': '*',
22 'api-key': process.env.DATA_API_KEY
23 },
24 data
25 };
26
27 let result = await axios(config);
28 let movie = result?.data?.document || {};
29
30 return {
31 title: params.title,
32 plot: movie.fullplot,
33 genres: movie.genres,
34 directors: movie.directors,
35 year: movie.year,
36 image: movie.poster
37 };
38};

The findOne query will filter the results by title. The title is extracted from the URL params provided as an argument to the loader function.

The data is returned as a document with the needed information to be presented like “full plot,” “poster,” “genres,” etc.

Let’s show the data with a simple html layout:

1export default function MovieDetails() {
2 let movie = useLoaderData();
3
4 return (
5 <div> <h1>{movie.title}</h1> {movie.plot} <br></br> <div styles="padding: 25% 0;" class="tooltip"> <li> Year </li> <Link class="tooltiptext" to={"../movies?filter=" + JSON.stringify({ "year": movie.year })}>{movie.year}</Link> </div> <br /> <div styles="padding: 25% 0;" class="tooltip"> <li> Genres </li> <Link class="tooltiptext" to={"../movies?filter=" + JSON.stringify({ "genres": movie.genres })}>{movie.genres.map(genre => { return genre + " | " })}</Link> </div> <br /> <div styles="padding: 25% 0;" class="tooltip"> <li> Directors </li> <Link class="tooltiptext" to={"../movies?filter=" + JSON.stringify({ "directors": movie.directors })}>{movie.directors.map(director => { return director + " | " })}</Link> </div> <br></br> <img src={movie.image}></img> </div>
6 );
7}

#facets/index.jsx File

MongoDB Atlas Search introduced a new feature complementing a very common use case in the text search world: categorising and allowing a faceted search. Facet search is a technique to present users with possible search criteria and allow them to specify multiple search dimensions. In a simpler example, it's the search criteria panels you see in many commercial or booking websites to help you narrow your search based on different available categories.

Additionally, to the different criteria you can have in a facet search, it adds better and much faster counting of different categories. To showcase this ability, I have created a new route called facets and added an additional page to show counts per genre under routes/facets/index.jsx. Let’s look at its loader function:

1export let loader = async ({ request }) => {
2 let pipeline = [
3 {
4 $searchMeta: {
5 facet: {
6 operator: {
7 range: {
8 path: "year",
9 gte: 1900
10 }
11 },
12 facets: {
13 genresFacet: {
14 type: "string",
15 path: "genres"
16 }
17 }
18 }
19 }
20 }
21 ];
22
23let data = JSON.stringify({
24 collection: "movies",
25 database: "sample_mflix",
26 dataSource: process.env.CLUSTER_NAME,
27 pipeline
28 });
29
30 let config = {
31 method: "post",
32 url: process.env.DATA_API_BASE_URL + "/action/aggregate",
33 headers: {
34 "Content-Type": "application/json",
35 "Access-Control-Request-Headers": "*",
36 "api-key": process.env.DATA_API_KEY
37 },
38 data
39 };
40
41 let movies = await axios(config);
42
43 return movies?.data?.documents[0];
44};

It uses a new stage called $searchMeta and two facet stages: one to make sure that movies start from a date (1900) and that we aggregate counts based on genres field:

1facet: {
2 operator: {
3 range: {
4 path: "year",
5 gte: 1900
6 }
7 },
8 facets: {
9 genresFacet: {
10 type: "string",
11 path: "genres"
12 }
13 }
14}

To use the facet search, we need to amend the index and add both fields to types for facet. Editing the index is easy through the Atlas visual editor. Just click [...] > “Edit with visual editor.” Facet Mappings

An output document of the search query will look like this:

1{"count":{"lowerBound":23494},
2"facet":{"genresFacet":{"buckets":[{"_id":"Drama","count":13771},
3 {"_id":"Comedy","count":7017},
4 {"_id":"Romance","count":3663},
5 {"_id":"Crime","count":2676},
6 {"_id":"Thriller","count":2655},
7 {"_id":"Action","count":2532},
8 {"_id":"Documentary","count":2117},
9 {"_id":"Adventure","count":2038},
10 {"_id":"Horror","count":1703},
11 {"_id":"Biography","count":1401}]
12 }}}

Once we route the UI page under facets demo, the table of genres in the UI will look as: Facet Search UI

#Adding Clickable Filters Using Routes

To make the application even more interactive, I have decided to allow clicking on any of the genres on the facet page and redirect to the movies search page with movies?filter={genres : <CLICKED-VALUE>}:

1<div class="tooltip">
2 <Link to={ "../movies?filter=" + JSON.stringify({ genres: bucket._id }) } > {bucket._id} </Link> <span class="tooltiptext">
3 Press to filter by "{bucket._id}" genre
4 </span>
5</div>

Now, every genre clicked on the facet UI will be redirected back to /movies?filter={generes: <VALUE-BUCKET._id>}—for example, /movies?filter={genres : "Drama"}.

This will trigger the movies/index.jsx loader function, where we will add the following condition:

1let filter = JSON.parse(url.searchParams.get("filter"));
2...
3
4 else if (filter) {
5 pipeline = [
6 {
7 "$match": filter
8 },{$limit : 100}
9 ]
10 }

Look how easy it is with the aggregation pipelines to switch between a regular match and a full text search.

With the same approach, we can add any of the presented fields as a search criteria—for example, clicking directors on a specific movie details page passing /movies?filter={directors: [ <values> ]}.

Click a filtered field (eg. "Directors")Redirect to filtered movies list
Sefty Last movie detailsRedirected movies filter

#Wrap Up

Remix has some clever and renewed concepts for building React-based web applications. Having server and client code coupled together inside moduled and parameterized by URL JS files makes developing fun and productive.

The MongoDB Atlas Data API comes as a great fit to easily access, search, and dice your data with simple REST-like API syntax. Overall, the presented stack reduces the amount of code and files to maintain while delivering best of class UI capabilities.

I recommend you check out the full code at the following github repo and get started with your new application using MongoDB Atlas today!

If you have questions, please head to our developer community website where the MongoDB engineers and the MongoDB community will help you build your next big idea with MongoDB.

Rate this article
MongoDB logo
© 2021 MongoDB, Inc.

About

  • Careers
  • Investor Relations
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2021 MongoDB, Inc.