Please participate in this survey to voice your opinion as a developer for an upcoming article!

You can read part 2 here.

MongoDB Realm is a serverless backend that allows you to not only write and read data easily but also provides easy ways to authenticate users, keep your data synchronized across multiple devices, and more.

In this tutorial, we'll learn how to create a MongoDB Realm application, add sample data to it, restrict data access based on user roles, then how to integrate the application with React. We'll create a website that shows restaurant reviews and allows users to create an account and add their own reviews.

You can find the code for this tutorial here.


Create a MongoDB Realm App

Create a MongoDB Cluster

Before we can create a MongoDB Realm App, we need to create a MongoDB Cluster. To do that, go to the Atlas portal. If you don't have an account or you're not already logged in, you need to do that first.

If you're not redirected to the Projects page, click on the logo at the top left.

Once you're on the Projects page, click on the New Project button at the right.

Then you will be asked to enter a project name. You can name it whatever you want. After that, you'll be asked to add members, if you need to. Once done, click on Create Project.

Once the project is created, you will be redirected to the Clusters page. Click on "Build a Cluster"

You'll be asked to pick a cluster plan. For this tutorial, you can just choose the free plan.

Then after that you can just click on "Create Cluster"

After this, your cluster will take some time to deploy. You need to wait until it's created and deploy, which can take a couple of minutes.

The next step will be to add a sample dataset to our Cluster. If you already have a dataset, you can add your own data instead.

To start adding data, click on Collections in the Cluster you created.

Then, click on Load a Sample Dataset.

A popup window will open to ask for confirmation. Once you confirm, a sample dataset will be installed in your cluster. This dataset contains a bunch of useful databases and collections for different use cases.

It will take a minute or two to finish installing the sample dataset. Once it's done, you'll see that now you have a few databases now.

We'll only be using the sample_restaurants database, so you can go ahead and delete the rest by clicking on the trash icon that appears when you hover a database name.

Now that our MongoDB Cluster is ready, let's go ahead and create a MongoDB Realm App.

Create a MongoDB Realm App

To go to MongoDB Realm, click on "Realm" in the tab bar next to "Atlas"

A dialog will show to start creating MongoDB Realm App. You'll need to enter a name for the Realm Application which can be whatever you want. Then, you'll need to choose a cluster to link the Realm App to. You'll need to choose the cluster we just created. Once you do that, click on Create Realm Application.

Next, we'll need to choose a collection from the cluster to add access to from the Realm app. To do that, click on Get Started under Add a Collection on the dashboard.

You'll have to pick the database, which is sample_restaurants. Then choose a collection, which will be restaurants.

Next, we need to select a permission template. Permission templates allow to easily restrict read and write access as necessary.

In the website we're creating, all users can read all data about restaurants, and they can write reviews in their own account.

For now, we'll just choose "Users can only read all data" from the dropdown. Once you're done, click "Add Collection." Next, click on neighborhoods from the sidebar and choose the same template then Add Collection.

Every time you make changes to the Realm App, you have to deploy it for changes to take effect. To deploy the changes we just made, click on "Review Draft & Deploy" in the blue banner at the top.

And that's it! We created a Realm App that's linked to our cluster and the collections in it. This will provide a serverless backend that allows us to retrieve and write data to our cluster easily.


Generate Schemas

To be able to query our collections and documents, and to be able to apply certain roles, permissions and restrictions, we need to generate Schema definitions for each of the collections. To do that, click on Schema in the sidebar.

Then, click on Generate Schema button. This will generate the schema based on the data that's already in the collection.

Under "Generate schema(s) for:" choose "all unconfigured collections" and for Sample type in "20" as we don't need to sample so many documents considering our data is simple. Then, click on Generate Schema.

Once it's done, you'll see the Schema generated with all the fields and their respective types.


Setup Authentication in Realm App

In our Realm app, we'll be using two Authentication providers:

  1. Anonymous Login: Allow the user to view all data without actually having to login.
  2. Email & Password Login: Users have to login with email and password to write their reviews.

This means that users have permission to read all data, but only write their own data.

In the Realm Portal, click on Authentication in the sidebar. You'll see a few Authentication providers that are all disabled.

We'll first enable "Allow users to login anonymously." Click on the edit button for this one and just toggle it on.

Then go back to the Authentication page. We'll now click on Edit for the second one which is "Email/Password."

First, enable the provider. Next, for "User Confirmation Method," choose "Automatically confirm users." MongoDB Realm provides a user confirmation workflow for your app, but in our case we don't need it.

Next comes "Password Reset Method." MongoDB Realm also provides a password reset method for your users. We won't be implementing it, but because we need to enter the configuration, just enter http://example.com/reset in "Password Reset URL."

Once you're done, click Save. Our users are now able to login with an email and password.

The last step for setting up authentications is to allow users who are logged in with email and password to write their own reviews. To do that, go to rules in the sidebar, then choose the restaurants collection, then click on "New Role" in the table.

A popup will open. You first need to enter the role name. We'll name it "Users"

Next, we'll need to enter "Apply When" condition, which means when should the user be considered as part of this role. We want users who are logged in with their email and password to be able to write their reviews. Enter the following:

{
  "%%user.data.email": {
    "%exists": true
  }
}

Then, for the "Document-Level Permissions" choose "Insert Documents." Once you're done, click "Done Editing."

Then, in the table click "Add Field" and type in "grades" and click the checkmark. Then check for both Read and Write for the User role. This adds the double restriction which is users are only able to write in grades, nothing else. As for Read, you can check for all fields. Then, click on the left Arrow under the "User" role name to give the User role a higher priority when matching the logged in user with the correct role. Once you're done, click on Save. The table should look like this:

And with this done, we can now anonymous and logged in users can read all data, but only users logged in can write their own reviews.

One last thing to do is make sure to click on Review Draft & Deploy for all the changes to take effect.

Now, we have our MongoDB Realm app ready for integration with React. Next, we'll go over how to integrate it with React and use all the functionalities we've been setting up.


React Setup

In case you don't have a React project ready, run the following to create one:

npx create-react-app restaurants-reviews
cd restaurants-reviews

Next, we'll install the MongoDB Realm Web SDK:

npm install --save realm-web

That's all we need to start using Realm with React. We'll also install React Bootstrap to make styling easier:

npm install react-bootstrap bootstrap@4.6.0

and React Router to add different pages:

npm install react-router-dom

Home Page

Let's first start by modifying creating the Home component which will be the home page. The home page will just show a list of restaurants and their ratings.

Create the file src/pages/Home.js and the following basic component:

function Home () {
	return (
    	<div></div>
    )
}

export default Home

For now, it's just a component that shows a <div> element. We need to make it show a list of restaurants instead.

Since we're going to fetch the restaurants from our MongoDB Realm App later on, we'll be using a state for restaurants:

function Home () {
	const [restaurants, setRestaurants] = useState([])
    //...
}

Then, we'll loop over the restaurants and display them:

<div className="mt-3">
    {restaurants.map((restaurant) => (
        <RestaurantCard key={restaurant._id} restaurant={restaurant} />
        ))
	}
</div>

Let's create src/components/RestaurantCard.js with the following content:

import { Badge } from 'react-bootstrap'
import Card from 'react-bootstrap/Card'

function RestaurantCard ({restaurant}) {
    //get average of grades
    let sum = 0;
    restaurant.grades.forEach(element => {
        sum += element.score
    });
    const avg = Math.round(sum / (restaurant.grades.length))
    return (
        <Card className="m-3">
            <Card.Body>
                <Card.Title>{restaurant.name} <Badge variant="warning">{avg}</Badge></Card.Title>
            </Card.Body>
        </Card>
    )
}

export default RestaurantCard

We're first calculating the average grade for the restaurant, then we're just displaying a card with the restaurant's name and the average grade.

So, our home page should show a list of cards with restaurant names and grades. What's left is to actually link it to the data in our Realm app.

Let's go over how to connect to Realm Apps first. You first need an App ID. You'll find the App ID on the Dashboard or you can click the copy icon in the sidebar.

Then, create a .env file in the root directory with the following content:

REACT_APP_REALM_APP_ID=<YOUR_APP_ID>

Make sure to replace <YOUR_APP_ID> with the App ID you copied. This helps changing App IDs easy just by changing it in .env.

Back to src/pages/Home.js, we first need to import the SDK:

import * as Realm from 'realm-web'

Then, initialize the Realm App:

const app = new Realm.App({id: process.env.REACT_APP_REALM_APP_ID})

Notice we're using the environment variable we set earlier.

Then inside the Home component, we'll use useEffect to fetch the data on first render:

useEffect(() => {

}, [])

Inside, we'll log in the user anonymously and then fetch the restaurants data. Since earlier we allowed all users to read all data, even users who aren't logged in can read the data.

To login a user anonymously:

useEffect(() => {
	async function getData () {
    	const user = await app.logIn(Realm.Credentials.anonymous())
    }
    
    getData();
}, [])

After that, we'll get the MongoDB client for our collection using the user we just logged in:

const client = app.currentUser.mongoClient('mongodb-atlas')

As you can tell, by using app.currentUser we're referring to the currently logged-in user. Then, we get the MongoDB client for that user. This means that the access to the data is restricted based on the user that is logged in, just like we defined above.

Next step would be to get the restaurants from restaurants collection and set the restaurants state:

const rests = client.db('sample_restaurants').collection('restaurants')
setRestaurants((await rests.find()).slice(0, 10))

And with this, our code will display the restaurants once we retrieve them from MongoDB Realm App. We'll add also some loading to make sure we can see the loading:

const [restaurants, setRestaurants] = useState([])
const [loading, setLoading] = useState(true)

useEffect(() => {
	async function getData () {
    	//...
        const rests = client.db('sample_restaurants').collection('restaurants')
        setRestaurants((await rests.find()).slice(0, 10))
        setLoading(false)
    }
    
    if (loading) {
        getData();
    }
}, [loading])

return (
    <div className="mt-3">
            {loading && (
                <div className="text-center">
                    <Loading />
                </div>
            )}
            {restaurants.map((restaurant) => (
                <RestaurantCard key={restaurant._id} restaurant={restaurant} />
            ))}
        </div>
);

We'll also create src/components/Loading.js:

import { Spinner } from "react-bootstrap";

function Loading () {
    return (
        <Spinner animation="border" variant="primary">
            <span className="sr-only">Loading...</span>
        </Spinner>
    )
}

export default Loading

And that's it! The home page now is ready. Only thing left is to use react-router in src/App.js to ensure multiple pages:

import {
  BrowserRouter as Router,
  Switch,
  Route,
} from "react-router-dom"
import Home from "./pages/Home"
import 'bootstrap/dist/css/bootstrap.min.css'
import { Container } from "react-bootstrap"

function App() {

  return (
    <Router>
        <Container>
          <Switch>
            <Route path="/" component={Home} />
          </Switch>
        </Container>
    </Router>
  );
}

export default App;

Let's now run the server:

npm start

After some loading, you'll see the restaurants with their average grades:

Next, we'll create authentication forms to allow users to create accounts and login.

Authentication Page

Since the user just needs to enter the email and password to Sign up and Log in, we'll just create one Authentication component that changes behavior based on type prop that determines whether the form is being used to create an account or login.

Before we start, let's install Formik and Yup to make creating a form easier:

npm i formik yup

Then, create src/pages/Authentication.js with the following content:

import { Formik } from 'formik'
import { Button, Form } from 'react-bootstrap'
import * as yup from 'yup'
import { useState } from 'react'
import Loading from '../components/Loading'

const userSchema = yup.object().shape({
    email: yup.string().email().required(),
    password: yup.string().required().min(8)
})

function Authentication ({type = 'login'}) {
    const [loading, setLoading] = useState(false)

    async function submitHandler (values) {
        setLoading(true)
        //TODO handle login/create
    }

    return (
        <Formik 
            initialValues={{
                email: '',
                password: ''
            }}

            validationSchema={userSchema}

            onSubmit={submitHandler}
        >
            {({errors, touched, handleSubmit, values, handleChange}) => (
                <Form noValidate onSubmit={handleSubmit}>
                    {loading && <Loading />}
                    {!loading && (<div>
                        <h1>{type === 'login' ? 'Login' : 'Sign Up'}</h1>
                        <Form.Row>
                            <Form.Label>Email</Form.Label>
                            <Form.Control type="email" name="email" value={values.email} onChange={handleChange} 
                            isValid={touched.email && !errors.email} />
                            <Form.Control.Feedback>{errors.email}</Form.Control.Feedback>
                        </Form.Row>
                        <Form.Row>
                            <Form.Label>Password</Form.Label>
                            <Form.Control type="password" name="password" value={values.password} onChange={handleChange} 
                            isValid={touched.password && !errors.password} />
                            <Form.Control.Feedback>{errors.password}</Form.Control.Feedback>
                        </Form.Row>
                        <div className="text-center mt-2">
                            <Button variant="primary" type="submit">Submit</Button>
                        </div>
                    </div>)}
                </Form>
            )}
        </Formik>
    )
}

export default Authentication

We're using Formik to create a form that has two fields, email, and password. We're also using yup it to create a validation schema. On form submit if everything is valid, the function submitHandler will run which accepted the values object.

Inside submitHandler, we need to check the type prop. If it equals create, then we need to create a new user and login the user after that. If it's login then we just need to login the user.

But before we start, as it will be a hassle to use the user object, the MongoDB client, and the Realm app, let's create a Context that allows us to use the same data throughout the components easily.

Create src/MongoContext.js with the following content:

import React from 'react'

const MongoContext = React.createContext({
    app: null,
    client: null,
    user: null,
    setApp: () => {},
    setClient: () => {},
    setUser: () => {}
})

export default MongoContext

We're creating a Context that has the objects app, client, and user and their setter functions setApp, setClient and setUser.

Next, let's move declarations and intialization of user, app and client that we did in Home to App:

const [client, setClient] = useState(null)
  const [user, setUser] = useState(null)
  const [app, setApp] = useState(new Realm.App({id: process.env.REACT_APP_REALM_APP_ID}))

  useEffect(() => {
    async function init () {
      if (!user) {
        setUser(app.currentUser ? app.currentUser : await app.logIn(Realm.Credentials.anonymous()))
      }

      if (!client) {
        setClient(app.currentUser.mongoClient('mongodb-atlas'))
      }
    }

    init();
  }, [app, client, user])

As you can see, we're creating states for each of them and setting them in App. Then, we'll wrap our routes with MongoContext.Provider:

return (
    <Router>
      <MongoContext.Provider value={{app, client, user, setClient, setUser, setApp}}>
      	<Container>
          <Switch>
            <Route path="/" component={Home} />
          </Switch>
        </Container>
      </MongoContext.Provider>
     </Router>
  );

Now, we need to pass the context to each of the components using MongoContext.Consumer. To avoid repetition, let's create a function inside App that does this:

function renderComponent (Component, additionalProps = {}) {
    return <MongoContext.Consumer>{(mongoContext) => <Component mongoContext={mongoContext} {...additionalProps} />}</MongoContext.Consumer>
  }

This will wrap a component with MongoContext.Consumer then pass it the mongoContext prop, which will hold all the objects we're storing in the context and their setters.

Back to the return statement in App, instead of passing component={Home} to the route, we'll pass a render function:

<Route path="/" render={() => renderComponent(Home)} />

Now, we have a context that holds all the objects and their setters, then we're passing it to a route's component.

Let's make changes in src/pages/Home.js where instead of initialing app, user, and client, it'll receive them as props:

import { useEffect, useState } from 'react'
import RestaurantCard from '../components/RestaurantCard'
import Loading from '../components/Loading'

function Home ({mongoContext: {client, user}}) {
    const [restaurants, setRestaurants] = useState([])
    const [loading, setLoading] = useState(true)

    useEffect(() => {
        async function getData () {
            const rests = client.db('sample_restaurants').collection('restaurants')
            setRestaurants((await rests.find()).slice(0, 10))
            setLoading(false)
        }

        if (loading && user && client) {
            getData()
        }
    }, [client, loading, user])

    return (
        <div className="mt-3">
            {loading && (
                <div className="text-center">
                    <Loading />
                </div>
            )}
            {restaurants.map((restaurant) => (
                <RestaurantCard key={restaurant._id} restaurant={restaurant} />
            ))}
        </div>
    )
}

export default Home

If you try running the server and going to the website, you'll see that everything is working perfectly as before.

Back to the Authentication component, we'll now pass it the mongoContext prop:

function Authentication ({mongoContext: {app, user, setUser}, type = 'login'})

Inside submitHandler, if the type is create we'll register a new user, then for both types we'll login the user with their credentials:

async function submitHandler (values) {
        setLoading(true)
        if (type === 'create') {
            //create
            await app.emailPasswordAuth.registerUser(values.email, values.password);
        }

        //login user and redirect to home
        const credentials = Realm.Credentials.emailPassword(values.email, values.password);
        setUser(await app.logIn(credentials))
        setLoading(false)
    }

As you can see, we're using app and setUser from the context. When we use setUser, the user will be updated for all components using the context.

Last thing we need to add is to redirect the user if they're already logged in. To do that, first create src/utils.js which will hold the function isAnon to determine whether the user is logged in:

module.exports = {
    isAnon: function (user) {
        return !user || user.identities[0].providerType === 'anon-user'
    }
}

Where providerType will be anon-user if the user is not logged in.

Then, inside Authentication, we'll get a history instance using useHistory from react-router:

const history = useHistory()

Then, whenever the user in the context changes, we'll check if the user is logged in and then we'll redirect to home if true.

useEffect(() => {	
	if (!isAnon(user)) {
		history.push('/')
	}
}, [history, user])

Our Authentication component is now done! Let's add signin and signup routes in src/App.js:

<Route path="/signup" render={() => renderComponent(Authentication, {type: 'create'})} />
<Route path="/signin" render={() => renderComponent(Authentication)} />
<Route path="/" render={() => renderComponent(Home)} />

We'll also need a LogOut page so create src/pages/Logout.js with the following content:

import { useEffect } from "react"
import Loading from "../components/Loading"
import * as Realm from 'realm-web'
import { useHistory } from "react-router"
import { isAnon } from "../utils"

function LogOut ({mongoContext: {app, setUser, setClient}}) {
    const history = useHistory()

    if (isAnon()) {
        history.push('/')
    }

    useEffect(() => {
        async function logout () {
            await app.currentUser.logOut()
            //login anon user
            setUser(await app.logIn(Realm.Credentials.anonymous()))
            //set new client
            setClient(app.currentUser.mongoClient('mongodb-atlas'))
        }

        logout()
    }, [app, setClient, setUser])

    return (
        <Loading />
    )
}

export default LogOut

We're first checking if the user is already not logged in and we're redirecting them to homepage if that's the case. Then, we're displaying the loading component and inside useEffect we're logging the user out using:

await app.currentUser.logOut()

After that, we're setting the user as anonymous user again and reinitializing the MongoDB Client:

//login anon user
setUser(await app.logIn(Realm.Credentials.anonymous()))
//set new client
setClient(app.currentUser.mongoClient('mongodb-atlas'))

And with that, we have our log out page. We just need to add it to the routes in src/App.js:

<Route path="/signup" render={() => renderComponent(Authentication, {type: 'create'})} />
<Route path="/signin" render={() => renderComponent(Authentication)} />
<Route path="/logout" render={() => renderComponent(LogOut)} />
<Route path="/" render={() => renderComponent(Home)} />

Lastly, we'll create a src/components/Navigation.js component to show a navigation bar with our links:

import { Nav, Navbar } from "react-bootstrap"
import { Link } from "react-router-dom"
import { isAnon } from "../utils"


function Navigation ({user}) {
    const loggedIn = !isAnon(user)
    return (
        <Navbar bg="light" expand="lg">
            <Navbar.Brand href="#home">Restaurant Reviews</Navbar.Brand>
            <Navbar.Toggle aria-controls="basic-navbar-nav" />
            <Navbar.Collapse id="basic-navbar-nav">
                <Nav className="mr-auto">
                    <Link to="/" className="mx-2">Home</Link>
                    {!loggedIn && <Link to="/signup" className="mx-2">Sign Up</Link>}
                    {!loggedIn && <Link to="/signin" className="mx-2">Sign In</Link>}
                    {loggedIn && <Link to="/logout" className="mx-2">Log out</Link>}
                </Nav>
            </Navbar.Collapse>
        </Navbar>
    )
}

export default Navigation

We're passing it the user prop, then we're checking if the user is logged in, we'll show the log out link. If not, we'll show the sign in and sign up links.

Add the Navigation component in src/App.js:

return (
    <Router>
      <Navigation user={user} />
      <MongoContext.Provider value={{app, client, user, setClient, setUser, setApp}}>
      //...
)

We're done! Run the server if you aren't already:

npm start

You'll see we have a navigation bar that shows the sign in and sign up links when we're not logged in. Try to sign up, log out, sign it, do different things. To check if the users are actually being created, on the Realm platform, click on "App Users" in the sidebar. You'll see a list of users with the type of user either Anonymous or Email/Password.


Conclusion

In the next part, we'll add a form for users to create their own reviews. We'll be able to test the permissions we added earlier and see how the users are restricted based on the roles we create.

You can read part 2 here.