# Next Gen Web Apps with Remix and MongoDB Atlas Data API

> Learn more about the GA version here.

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, we 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.

> Make sure to check out the live Remix and MongoDB demo application! You can find its source code in this dedicated GitHub repository.

## 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:

* Get started with Atlas and prepare a cluster to work with.

* Enable the Data API and save the API key.

* Load a sample data set into the cluster. (This application is using sample\_mflix for its demo.)

## Setting Up Remix Application

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

``` shell
npx create-remix@latest
```

The command will prompt for several settings. You can use 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:

``` shell
npm 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.

```
- app
    - routes
        - index.jsx
    - 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.

``` html

  
   
       
 * 
         Home
       
   
       
 * 
         Movies Search Demo
       
   
       
 * 
         Facet Search Demo
       
   
       
 * 
         GitHub
       
   
     

```

> 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:

```
- routes
  - movies
    - $title.jsx
    - index.jsx
```

The idea is that whenever our application is redirecting to `/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:

```
DATA_API_KEY=
DATA_API_BASE_URL=
CLUSTER_NAME=
```

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:
``` json
"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.

```shell
cd app/routes
mkdir movies
touch movies/index.jsx movies/\$title.jsx
```

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

``` javascript
import { Form, Link, useLoaderData , useSearchParams, useSubmit } from "remix";
const axios = require("axios");
 
export default function Movies() {
   let searchParams, setSearchParams] = useSearchParams();
   let submit = useSubmit();
   let movies = useLoaderData();
   let totalFound = movies.totalCount;
   let totalShow = movies.showCount;
 
   return (
       

           

MOVIES

           
                submit(e.currentTarget.form)}
                   id="searchBar" name="searchTerm" placeholder="Search movies..." />
               

Showing {totalShow} of total  {totalFound} movies found

           
           

               {movies.documents.map(movie => (
                   

                       {movie.title}
                   
               ))}
           

       

   );
}
```

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 `` 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.

```javascript
export let loader = async ({ request }) => {
    let pipeline = [{ $limit: 100 }];

    let data = JSON.stringify({
        collection: "movies",
        database: "sample_mflix",
        dataSource: process.env.CLUSTER_NAME,
        pipeline
    });

    let config = {
        method: 'post',
        url: `${process.env.DATA_API_BASE_URL}/action/aggregate`,
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Request-Headers': '*',
            'apiKey': process.env.DATA_API_KEY
        },
        data
    };

    let movies = await axios(config);
    let totalFound = await getCountMovies();

    return {
        showCount: movies?.data?.documents?.length,
        totalCount: totalFound,
        documents: movies?.data?.documents
    };
};

const getCountMovies = async (countFilter) => {
    let pipeline = countFilter ?
        [{ $match: countFilter }, { $count: 'count' }] :
        [{ $count: 'count' }];

    let data = JSON.stringify({
        collection: "movies",
        database: "sample_mflix",
        dataSource: process.env.CLUSTER_NAME,
        pipeline
    });

    let config = {
        method: 'post',
        url: `${process.env.DATA_API_BASE_URL}/action/aggregate`,
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Request-Headers': '*',
            'apiKey': process.env.DATA_API_KEY
        },
        data
    };

    let result = await axios(config);

    return result?.data?.documents[0]?.count;
}
```

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:

``` javascript 
let data = JSON.stringify({
    collection: "movies",
    database: "sample_mflix",
    dataSource: process.env.CLUSTER_NAME,
    pipeline
});

let config = {
    method: 'post',
    url: `${process.env.DATA_API_BASE_URL}/action/aggregate`,
    headers: {
        'Content-Type': 'application/json',
        'Access-Control-Request-Headers': '*',
        'apiKey': process.env.DATA_API_KEY
    },
    data
};

let 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:

``` javascript 
return result?.data?.documents[0]?.count;
```

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

``` shell
npm run dev
```

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

### Adding a Search Via Atlas Text Search
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.

Since we have a `
` Remix component submitting the form input, data typed into the input box will trigger a data reload. The `
`  reloads the loader function without refreshing the entire page. This will naturally resubmit the URL as `/movies?searchTerm=` 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:

``` javascript
let url = new URL(request.url);
let searchTerm = url.searchParams.get("searchTerm");
 
const pipeline = searchTerm ?
  
    {
      $search: {
        index: 'default',
        text: {
          query: searchTerm,
          path: {
            'wildcard': '*'
           }
        }
      }
    }, { $limit: 100 }, { "$addFields": { meta: "$$SEARCH_META" } }
  ] :
  [{ $limit: 100 }];
 ```
 
 In this case, the submission of a form will call the loader function again. If there was a `searchTerm`submitted in the URL, it will be extracted under the `searchTerm` variable and create a `$search` pipeline to interact with the Atlas Search text index.

``` javascript
text: {
   query: searchTerm,
   path: {
       'wildcard': '*'
   }
}
```

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). 

``` javascript 
{ "$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/` 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:

```javascript
import { Link, useLoaderData } from "remix";
import invariant from "tiny-invariant";
 
const axios = require('axios');
 
export let loader = async ({ params }) => {
   invariant(params.title, "expected params.title");
 
   let data = JSON.stringify({
       collection: "movies",
       database: "sample_mflix",
       dataSource: process.env.CLUSTER_NAME,
       filter: { title: params.title }
   });
 
   let config = {
       method: 'post',
       url: process.env.DATA_API_BASE_URL + '/action/findOne',
       headers: {
           'Content-Type': 'application/json',
           'Access-Control-Request-Headers': '*',
           'apiKey': process.env.DATA_API_KEY
       },
       data
   };
 
   let result = await axios(config);
   let movie = result?.data?.document || {};
 
   return {
       title: params.title,
       plot: movie.fullplot,
       genres: movie.genres,
       directors: movie.directors,
       year: movie.year,
       image: movie.poster
   };
};
```

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: 

``` javascript
export default function MovieDetails() {
   let movie = useLoaderData();
 
   return (
       <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>
   );
}
```

## `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, we 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:

``` javascript
export let loader = async ({ request }) => {
 let pipeline = 
   {
     $searchMeta: {
       facet: {
         operator: {
           range: {
             path: "year",
             gte: 1900
           }
         },
         facets: {
           genresFacet: {
             type: "string",
             path: "genres"
           }
         }
       }
     }
   }
 ];
 
let data = JSON.stringify({
   collection: "movies",
   database: "sample_mflix",
   dataSource: process.env.CLUSTER_NAME,
   pipeline
 });
 
 let config = {
   method: "post",
   url: process.env.DATA_API_BASE_URL + "/action/aggregate",
   headers: {
     "Content-Type": "application/json",
     "Access-Control-Request-Headers": "*",
     "apiKey": process.env.DATA_API_KEY
   },
   data
 };
 
 let movies = await axios(config);
 
 return movies?.data?.documents[0];
};
```

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:

``` javascript
facet: {
 operator: {
   range: {
     path: "year",
     gte: 1900
   }
 },
 facets: {
   genresFacet: {
     type: "string",
     path: "genres"
   }
 }
}
```

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:

``` json
{"count":{"lowerBound":23494},
"facet":{"genresFacet":{"buckets":{"_id":"Drama","count":13771},
                                   {"_id":"Comedy","count":7017},
                                   {"_id":"Romance","count":3663},
                                   {"_id":"Crime","count":2676},
                                   {"_id":"Thriller","count":2655},
                                   {"_id":"Action","count":2532},
                                   {"_id":"Documentary","count":2117},
                                   {"_id":"Adventure","count":2038},
                                   {"_id":"Horror","count":1703},
                                   {"_id":"Biography","count":1401}]
                        }}}
```

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, we 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>}`:

```html
<div class="tooltip">
  <Link to={ "../movies?filter=" + JSON.stringify({ genres: bucket._id }) } >
    {bucket._id}
  </Link>
  <span class="tooltiptext">
    Press to filter by "{bucket._id}" genre
  </span>
</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:

```javascript
let filter = JSON.parse(url.searchParams.get("filter"));
...
 
 else if (filter) {
   pipeline = 
     {
       "$match": filter
     },{$limit : 100}
   ]
 }
```

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 details ||

## 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.

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.
>
>