# How to Build a Healthcare Interoperability Microservice Using FHIR and MongoDB

# How to Build a Healthcare Interoperability Microservice Using FHIR and MongoDB

Interoperability refers to a system’s or software's capability to exchange and utilize information. Modern interoperability standards, like Fast Healthcare interoperability Resources (or FHIR), precisely define how data should be communicated. Like most current standards, FHIR uses REST APIs which are set in JSON format. However, these standards do not set how data should be stored, providing software vendors with flexibility in managing information according to their preferences.

This is where MongoDB's approach comes into play — data that is accessed together should be stored together. The compatibility between the FHIR’s resource format and MongoDB's document model allows the data to be stored exactly as it should be communicated. This brings several benefits, such as removing the need for any middleware/data processing tool which decreases development complexity and accelerates read/write operations. 

Additionally, MongoDB can also allow you to create a FHIR-compliant Atlas data API. This benefits healthcare providers using software vendors by giving them control over their data without complex integrations. It reduces integration complexity by handling data processing at a platform level. MongoDB's app services also offer security features like authentication. This, however, is not a full clinical data repository nor is it meant to replace one. Rather, this is yet another integration capability that MongoDB has.

In this article, we will walk you through how you can expose the data of FHIR resources through Atlas Data API to two different users with different permissions.

## Scenario

- Dataset: We have a simple dataset where we have modeled the data using FHIR-compliant schemas. These resources are varied: patients, locations, practitioners, and appointments.
- We have two users groups that have different responsibilities:
    - The first is a group of healthcare providers. These individuals work in a specific location and should only have access to the appointments in said location.
    - The second is a group that works at a healthcare agency. These individuals analyze the appointments from several centers. They should not be able to look at personal identifiable information (or PII).

## Prerequisites

- Deploy an M0+ Atlas cluster. 
- Install Python3 along with PyMongo and Mimesis modules to generate and insert documents.

## Step 1: Insert FHIR documents into the database

Clone this GitHub repository on your computer. 

- Add your connection string on the config.py file. You can find it by following the instructions in our docs.
- Execute the files: locGen.py,pracGen.py, patientGen.py, and ProposedAppointmentGeneration.py in that order.

> Note: The last script will take a couple of minutes as it creates the appointments with the relevant information from the other collections.

Before continuing, you should check that you have a new “FHIR” database along with four collections inside it:

- Locations with 22 locations
- Practitioners with 70 documents
- Patients with 20,000 documents
- Appointments with close to 7,000 documents

## Step 2: Create an App Services application

After you’ve created a cluster and loaded the sample dataset, you can create an application in Atlas App Services. 

Follow the steps to create a new App Services application if you haven’t done so already.

I used the name “FHIR-search” and chose the cluster “FHIR” that I’ve already loaded the sample dataset into.

 or from below.

```javascript
exports = async function(request, response) {
    const queryParams = request.query;
    const collection = context.services.get("mongodb-atlas").db("FHIR").collection("appointments");

    const query = {};
    const sort = {};
    const project = {};
    const codeParams = {};
    const aggreg = ];
    const pageSize = 20;
    const limit={};
    let tot = true;
    let dynamicPageSize = null;
    const URL = 'https://fakeurl.com/endpoint/appointment'//put your http endpoint URL here

    const FieldMap = {
        'actor': 'participant.actor.reference',
        'date': 'start', 
        'identifier':'_id',
        'location': 'location.reference', 
        'part-status': 'participant.0.actor.status',
        'patient':'participant.0.actor.reference',
        'practitioner': 'participant.1.actor.reference', 
        'status': 'status', 
    };

    for (const key in queryParams) {
        switch (key) {
            case "actor":
                query[FieldMap[key]] = new BSON.ObjectId(queryParams[key]);
                break;
            case "date":
                const dateParams = queryParams[key].split(",");
                const dateFilters = dateParams.map((dateParam) => {
                    const firstTwoChars = dateParam.substr(0, 2);
                    const dateValue = dateParam.slice(2);
                    if (firstTwoChars === "ge" || firstTwoChars === "le") {
                        const operator = firstTwoChars === "ge" ? "$gte" : "$lte";
                        return { ["start"]: { [operator] : new Date(dateValue) } };
                    }
                    return null;
                });
                query["$and"] = dateFilters.filter((filter) => filter !== null);
                break;
            case "identifier":
                query[FieldMap[key]] = new BSON.ObjectId(queryParams[key]);
                break;
            case "location":
                try {
                    query[FieldMap[key]] = new BSON.ObjectId(queryParams[key]);
                } catch (error) {
                    const locValues = queryParams[key].split(","); 
                    query[FieldMap[key]] = { $in: locValues }; 
                }
                break;
            case "location:contains"  :
                try {
                    query[FieldMap[key]] = {"$regex": new BSON.ObjectId(queryParams[key]), "$options": "i"};
                } catch (error) {
                    query[FieldMap[key]] = {"$regex": queryParams[key], "$options": "i"};
                }
                break;
            case "part-status":
                query[FieldMap[key]] = new BSON.ObjectId(queryParams[key]);
                break;
            case "patient":
                query[FieldMap[key]] = new BSON.ObjectId(queryParams[key]);
                break;
            case "practitioner":
                query[FieldMap[key]] = new BSON.ObjectId(queryParams[key]);
                break;
            case "status":
                const statusValues = queryParams[key].split(","); 
                query[FieldMap[key]] = { $in: statusValues }; 
                break;
            case "_count":
                dynamicPageSize = parseInt(queryParams[key]);
                break;
            case "_elements":
                const Params = queryParams[key].split(",");
                for (const param of Params) {
                    if (FieldMap[param]) {
                        project[FieldMap[param]] = 1;
                    }
                }
                break;
            case "_sort":
                // sort logic
                const sortDirection = queryParams[key].startsWith("-") ? -1 : 1;
                const sortField = queryParams[key].replace(/^-/, ''); 
                sort[FieldMap[sortField]] = sortDirection;
                break;
            case "_maxresults":
                // sort logic
                limit["_maxresults"]=parseInt(queryParams[key])
                break;
            case "_total":
                tot = false;
                break;
            default:
                // Default case for other keys
                codeParams[key] = queryParams[key];
                break;
        }
    }

    let findResult;
    const page = parseInt(codeParams.page) || 1;
    if (tot) {
        aggreg.push({'$match':query});
        if(Object.keys(sort).length > 0){
            aggreg.push({'$sort':sort});
        } else {
            aggreg.push({'$sort':{"start":1}});
        }
        if(Object.keys(project).length > 0){
            aggreg.push({'$project':project});
        }
        if(Object.keys(limit).length > 0){
            aggreg.push({'$limit':limit["_maxresults"]});
        }else{
            aggreg.push({'$limit':(dynamicPageSize||pageSize)*page});
        }
        try {
            //findResult = await collection.find(query).sort(sort).limit((dynamicPageSize||pageSize)*pageSize).toArray();
            findResult = await collection.aggregate(aggreg).toArray();
        } catch (err) {
            console.log("Error occurred while executing find:", err.message);
            response.setStatusCode(500);
            response.setHeader("Content-Type", "application/json");
            return { error: err.message };
        }
    } else {
        findResult = [];
    }
    let total
    if(Object.keys(limit).length > 0){
        total=limit["_maxresults"];
    }else{
        total = await collection.count(query);
    }
    const totalPages = Math.ceil(total / (dynamicPageSize || pageSize));
    const startIdx = (page - 1) * (dynamicPageSize || pageSize);
    const endIdx = startIdx + (dynamicPageSize || pageSize);
    const resultsInBundle = findResult.slice(startIdx, endIdx);

    const bundle = {
        resourceType: "Bundle",
        type: "searchset",
        total:total,
        link:[],
        entry: resultsInBundle.map((resource) => ({
            fullUrl: `${URL}?id=${resource._id}`, 
            resource,
            search: {
                mode: 'match'
            },
        })),
    };

    if (page <= totalPages) {
        if (page > 1 && page!==totalPages) {
            bundle.link = [
                { relation: "previous", url: `${URL}${getQueryString(queryParams,sort,page-1,dynamicPageSize || pageSize)}` },
                { relation: "self", url: `${URL}${getQueryString(queryParams,sort,page,dynamicPageSize || pageSize)}` },
                { relation: "next", url: `${URL}${getQueryString(queryParams,sort,page+1,dynamicPageSize || pageSize)}` },
            ];
        } else if(page==totalPages && totalPages!==1) {
            bundle.link = [
                { relation: "previous", url: `${URL}${getQueryString(queryParams,sort,page-1,dynamicPageSize || pageSize)}` },
                { relation: "self", url: `${URL}${getQueryString(queryParams,sort,page,dynamicPageSize || pageSize)}` }
            ];
        } else if(totalPages==1 || dynamicPageSize==0) {
            bundle.link = [
                { relation: "self", url: `${URL}${getQueryString(queryParams,null,0,0)}` },
            ];
        } else {
            bundle.link = [
                { relation: "self", url: `${URL}${getQueryString(queryParams,sort,page,dynamicPageSize || pageSize)}` },
                { relation: "next", url: `${URL}${getQueryString(queryParams,sort,page+1,dynamicPageSize || pageSize)}` },
            ];
        }
    }

    response.setStatusCode(200);
    response.setHeader("Content-Type", "application/json");
    response.setBody(JSON.stringify(bundle, null, 2));
};

// Helper function to generate query string from query parameters
function getQueryString(params,sort, p, pageSize) {

    let paramString = "";
    let queryString = "";

    if (params && Object.keys(params).length > 0) {
        paramString = Object.keys(params)
            .filter((key) => key !== "page" && key !== "_count")
            .map((key) => `${(key)}=${params[key]}`)
            .join("&");
    }

    if (paramString!==""){
        if (p > 1) {
            queryString = `?`+ paramString.replace(/ /g, "%20") + `&page=${(p)}&_count=${pageSize}`;
        } else {
            queryString += `?`+ paramString.replace(/ /g, "%20") +`&_count=${pageSize}`
        }
    } else if (p > 1) {
        queryString = `?page=${(p)}&_count=${pageSize}`;
    }

    return queryString;
}
```

- Make sure to change the fake URL in said function with the one that was just created from your HTTPS endpoint.

![Add Authentication to the Endpoint Function][3]

- Enable both “Fetch Custom User Data” and “Create User Upon Authentication.”
- Lastly, save the draft and deploy it.

![Publish the Endpoint Function][4]

Now, your API endpoint is ready and accessible! But if you test it, you will get the following authentication error since no authentication provider has been enabled.

```bash
curl --location --request GET https://.com/app//endpoint/appointment' \
 --header 'Content-Type: application/json' \

{"error":"no authentication methods were specified","error_code":"InvalidParameter","link":"https://realm.mongodb.com/groups/64e34f487860ee7a5c8fc990/apps/64e35fe30e434ffceaca4c89/logs?co_id=64e369ca7b46f09497deb46d"}
```

> Side note: To view the result without any security, you can go into your function, then go to the settings tab and set the authentication to system. However, this will treat any request as if it came from the system, so proceed with caution.

## Step 3.1: Enable JWT-based authentication

FHIR emphasizes the importance of secure data exchange in healthcare. While FHIR itself doesn't define a specific authentication protocol, it recommends using OAuth for web-centric applications and highlights the HL7 SMART App Launch guide for added context. This focus on secure authentication aligns with MongoDB Atlas's provision for JWT (JSON Web Tokens) as an authentication method, making it an advantageous choice when building FHIR-based microservices.

Then, to add authentication, navigate to the homepage of the App Services application. Click “Authentication” on the left-hand side menu and click the EDIT button of the row where the provider is Custom JWT Authentication.

![Enable JWT Authentication for the Endpoint][5]

JWT (JSON Web Token) provides a token-based authentication where a token is generated by the client based on an agreed secret and cryptography algorithm. After the client transmits the token, the server validates the token with the agreed secret and cryptography algorithm and then processes client requests if the token is valid.

In the configuration options of the Custom JWT Authentication, fill out the options with the following:

- Enable the Authentication Provider (Provider Enabled must be turned on).
- Keep the verification method as is (manually specify signing keys).
- Keep the signing algorithm as is (HS256).
- Add a new signing key.
    - Provide the signing key name.
        - For example, APITestJWTSigningKEY
    - Provide the secure key content (between 32 and 512 characters) and note it somewhere secure.
        - For example, FipTEgYJ6WfUEhCJq3e@pm8-TkE9*UZN
- Add two fields in the metadata fields.
    - The path should be metadata.group and the corresponding field should be group.
    - The path should be metadata.name and the corresponding field should be name.
- Keep the audience field as is (empty).

Below, you can find how the JWT Authentication Provider form has been filled accordingly.

![JWT Authentication Provider Example][6]

Save it and then deploy it.

After it’s deployed, you can see the secret that has been created in the [App Services Values. It’s accessible on the left side menu by clicking “Values.”

.

These are the steps to generate an encoded JWT:

- Visit jwt.io.
- On the right-hand side in the section Decoded, we can fill out the values. On the left-hand side, the corresponding Encoded JWT will be generated.
- In the Decoded section:
    - Keep the header section the same.
    - In the Payload section, set the following fields:
        - Sub
            - Represents the owner of the token
            - Provide value unique to the user
        - Metadata
            - Represents metadata information regarding this token and can be used for further processing in App Services
            - We have two sub fields here
                - Name
                    - Represents the username of the client that will initiate the API request
                    - Will be used as the username in App Services
                - Group
                    - Represents the group information of the client that we’ll use later for rule-based access
        - Exp
            - Represents when the token is going to expire
            - Provides a future time to keep expiration impossible during our tests
        - Aud
            - Represents the name of the App Services application that you can get from the homepage of your application in App Services
    - In the Verify Signature section:
        - Provide the same secret that you’ve already provided while enabling Custom JWT Authentication in Step 3.1.

Below, you can find how the values have been filled out in the Decoded section and the corresponding Encoded JWT that has been generated.

 defined, we were not able to access any data.

Even though the request is not successful due to the no rule definition, you can check out the App Users page to list authenticated users, as shown below. user01 was the name of the user that was provided in the metadata.name field of the JWT.

.

Otherwise, let’s create a role that will have access to all of the fields. 

- Navigate to the Rules section on the left-hand side of the menu in App Services.
- Choose the collection appointments on the left side of the menu.
- Click **readAll** on the right side of the menu, as shown below.

. As a demo, this won’t be presenting all of FHIR search capabilities. Instead, we will focus on the basic ones.

In our server, we will be able to respond to two types of inputs. First, there are the regular search parameters that we can see at the bottom of the resources’ page. And second, we will implement the Search Result Parameters that can modify the results of a performed search. Because of our data schema, not all will apply. Hence, not all were coded into the function.

More precisely, we will be able to call the search parameters: actor, date, identifier, location, part-status, patient, practitioner, and status. We can also call the search result parameters: _count, _elements, _sort, _maxresults, and _total, along with the page parameter. Please refer to the FHIR documentation to see how they work. 

Make sure to test both users as the response for each of them will be different. Here, you have a couple of examples. To keep it short, I’ll set the page to a single appointment by adding ?_count=1 to the URL.

Healthcare provider:

```
curl --request GET '{{URL}}?_count=1' \  --header 'jwtTokenString: {{hcproviderJWT}}' \  --header 'Content-Type: application/json'

HTTP/1.1 200 OK
content-encoding: gzip
content-type: application/json
strict-transport-security: max-age=31536000; includeSubdomains;
vary: Origin
x-appservices-request-id: 64e5e47e6dbb75dc6700e42c
x-frame-options: DENY
date: Wed, 23 Aug 2023 10:50:38 GMT
content-length: 671
x-envoy-upstream-service-time: 104
server: mdbws
x-envoy-decorator-operation: baas-main.baas-prod.svc.cluster.local:8086/*
connection: close

{
  "resourceType": "Bundle",
  "type": "searchset",
  "total": 384,
  "link": 
    {
      "relation": "self",
      "url": "https://fakeurl.com/endpoint/appointment"
    },
    {
      "relation": "next",
      "url": "https://fakeurl.com/endpoint/appointment?page=2\u0026_count=1"
    }
  ],
  "entry": [
    {
      "fullUrl": "https://fakeurl.com/endpoint/appointment?id=64e35896eaf6edfdbe5f22be",
      "resource": {
        "_id": "64e35896eaf6edfdbe5f22be",
        "resourceType": "Appointment",
        "status": "proposed",
        "created": "2023-08-21T14:29:10.312Z",
        "start": "2023-08-21T14:29:09.535Z",
        "description": "Breast Mammography Screening",
        "serviceType": [
          {
            "coding": [
              {
                "system": "http://snomed.info/sct",
                "code": "278110001",
                "display": "radiographic imaging"
              }
            ],
            "text": "Mammography"
          }
        ],
        "participant": [
          {
            "actor": {
              "reference": "64e354874f5c09af1a8fc2b6",
              "display": [
                {
                  "given": [
                    "Marta"
                  ],
                  "family": "Donovan"
                }
              ]
            },
            "required": true,
            "status": "needs-action"
          },
          {
            "actor": {
              "reference": "64e353d80727df4ed8d00839",
              "display": [
                {
                  "use": "official",
                  "family": "Harrell",
                  "given": [
                    "Juan Carlos"
                  ]
                }
              ]
            },
            "required": true,
            "status": "accepted"
          }
        ],
        "location": {
          "reference": "64e35380f2f2059b24dafa60",
          "display": "St. Barney clinic"
        }
      },
      "search": {
        "mode": "match"
      }
    }
  ]
}
```

Healthcare agency:

```
curl --request GET '{{URL}}?_count=1' \  --header 'jwtTokenString: {{hcagencyJWT}}' \  --header 'Content-Type: application/json'\

HTTP/1.1 200 OK
content-encoding: gzip
content-type: application/json
strict-transport-security: max-age=31536000; includeSubdomains;
vary: Origin
x-appservices-request-id: 64e5e4eee069ab6f307d792e
x-frame-options: DENY
date: Wed, 23 Aug 2023 10:52:30 GMT
content-length: 671
x-envoy-upstream-service-time: 162
server: mdbws
x-envoy-decorator-operation: baas-main.baas-prod.svc.cluster.local:8086/*
connection: close

{
  "resourceType": "Bundle",
  "type": "searchset",
  "total": 6720,
  "link": [
    {
      "relation": "self",
      "url": "https://fakeurl.com/endpoint/appointment"
    },
    {
      "relation": "next",
      "url": "https://fakeurl.com/endpoint/appointment?page=2\u0026_count=1"
    }
  ],
  "entry": [
    {
      "fullUrl": "https://fakeurl.com/endpoint/appointment?id=64e35896eaf6edfdbe5f22be",
      "resource": {

        "_id": "64e35896eaf6edfdbe5f22be",
        "resourceType": "Appointment",
        "status": "proposed",
        "created": "2023-08-21T14:29:10.312Z",
        "start": "2023-08-21T14:29:09.535Z",
        "description": "Breast Mammography Screening",
        "serviceType": [

          {

            "coding": [

              {

                "system": "http://snomed.info/sct",
                "code": "278110001",
                "display": "radiographic imaging"
              }
            ],
            "text": "Mammography"
          }
        ],
        "participant": [

          {

            "actor": {

              "reference": "64e354874f5c09af1a8fc2b6",
              "display": [

                {

                  "given": [

                    "Marta"
                  ],
                  "family": "Donovan"
                }
              ]
            },
            "required": true,
            "status": "needs-action"
          },
          {

            "actor": {

              "reference": "64e353d80727df4ed8d00839",
              "display": [

                {

                  "use": "official",
                  "family": "Harrell",
                  "given": [

                    "Juan Carlos"
                  ]
                }
              ]
            },
            "required": true,
            "status": "accepted"
          }
        ],
        "location": {

          "reference": "64e35380f2f2059b24dafa60",
          "display": "St. Barney clinic"
        }
      },
      "search": {

        "mode": "match"
      }
    }
  ]
}
```

Please note the difference on the total number of documents fetched as well as the participant.actor.display fields missing for the agency user.

## Step 6: How to call the microservice from an application

The calls that were shown up to this point were from API platforms such as Postman or Visual Studio’s REST client. However, for security reasons, when putting this into an application such as a React.js application, then the calls might be blocked by the CORS policy. To avoid this, we need to authenticate our data API request. You can read more on how to manage your user sessions [in our docs. But for us, it should be as simple as sending the following request:

```bash
curl -X POST 'https://..realm.mongodb.com/api/client/v2.0/app//auth/providers/custom-token/login' \
  --header 'Content-Type: application/json' \
  --data-raw '{
          "token": ""
  }'
```

This will return something like:

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJiYWFzX2RldmljZV9pZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsImJhYXNfZG9tYWluX2lkIjoiNWVlYTg2NjdiY2I0YzgxMGI2NTFmYjU5IiwiZXhwIjoxNjY3OTQwNjE4LCJpYXQiOjE2Njc5Mzg4MTgsImlzcyI6IjYzNmFiYTAyMTcyOGI2YzFjMDNkYjgzZSIsInN0aXRjaF9kZXZJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsInN0aXRjaF9kb21haW5JZCI6IjVlZWE4NjY3YmNiNGM4MTBiNjUxZmI1OSIsInN1YiI6IjYzNmFiYTAyMTcyOGI2YzFjMDNkYjdmOSIsInR5cCI6ImFjY2VzcyJ9.pyq3nfzFUT-6r-umqGrEVIP8XHOw0WGnTZ3-EbvgbF0",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJiYWFzX2RhdGEiOm51bGwsImJhYXNfZGV2aWNlX2lkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwiYmFhc19kb21haW5faWQiOiI1ZWVhODY2N2JjYjRjODEwYjY1MWZiNTkiLCJiYWFzX2lkIjoiNjM2YWJhMDIxNzI4YjZjMWMwM2RiODNlIiwiYmFhc19pZGVudGl0eSI6eyJpZCI6IjYzNmFiYTAyMTcyOGI2YzFjMDNkYjdmOC1ud2hzd2F6ZHljbXZycGVuZHdkZHRjZHQiLCJwcm92aWRlcl90eXBlIjoiYW5vbi11c2VyIiwicHJvdmlkZXJfaWQiOiI2MjRkZTdiYjhlYzZjOTM5NjI2ZjU0MjUifSwiZXhwIjozMjQ0NzM4ODE4LCJpYXQiOjE2Njc5Mzg4MTgsInN0aXRjaF9kYXRhIjpudWxsLCJzdGl0Y2hfZGV2SWQiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJzdGl0Y2hfZG9tYWluSWQiOiI1ZWVhODY2N2JjYjRjODEwYjY1MWZiNTkiLCJzdGl0Y2hfaWQiOiI2MzZhYmEwMjE3MjhiNmMxYzAzZGI4M2UiLCJzdGl0Y2hfaWRlbnQiOnsiaWQiOiI2MzZhYmEwMjE3MjhiNmMxYzAzZGI3Zjgtbndoc3dhemR5Y212cnBlbmR3ZGR0Y2R0IiwicHJvdmlkZXJfdHlwZSI6ImFub24tdXNlciIsInByb3ZpZGVyX2lkIjoiNjI0ZGU3YmI4ZWM2YzkzOTYyNmY1NDI1In0sInN1YiI6IjYzNmFiYTAyMTcyOGI2YzFjMDNkYjdmOSIsInR5cCI6InJlZnJlc2gifQ.h9YskmSpSLK8DMwBpPGuk7g1s4OWZDifZ1fmOJgSygw",
  "user_id": "636aba021728b6c1c03db7f9"
}
```

These tokens will allow your application to request data from your FHIR microservice. You will just need to replace the header 'jwtTokenString: {{JWT}}' with 'Authorization: Bearer {{token above}}', like so:

```
curl --request GET {{URL}} \  --header 'Authorization: Bearer {{token above}}' \ 
--header 'Content-Type: application/json'

{"error": "no matching rule found" }
```

You can find additional information in our docs for authenticating Data API requests.

## Summary

In conclusion, interoperability plays a crucial role in enabling the exchange and utilization of information within systems and software. Modern standards like Fast Healthcare Interoperability Resources (FHIR) define data communication methods, while MongoDB's approach aligns data storage with FHIR's resource format, simplifying integration and improving performance. 

MongoDB's capabilities, including Atlas Data API, offer healthcare providers and software vendors greater control over their data, reducing complexity and enhancing security. However, it's important to note that this integration capability complements rather than replaces clinical data repositories. In the previous sections, we explored how to: 

- Generate your own FHIR data.
- Configure serverless functions along with Custom JWT Authentication to seamlessly integrate user-specific information. 
- Implement precise data access control through roles and filters. 
- Call the configured APIs directly from the code.

Are you ready to dive in and leverage these capabilities for your projects? Don't miss out on the chance to explore the full potential of MongoDB Atlas App Services. Get started for free by provisioning an M0 Atlas instance and creating your own App Services application. 

Should you encounter any roadblocks or have questions, our vibrant developer forums are here to support you every step of the way. 

  [1]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blt1f8674230e64660c/652eb2153b618bf623f212fa/image12.png
  [2]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blte32e786ba5092f3c/652eb2573fc0c855d1c9446c/image13.png
  [3]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/bltdbf6696269e8c0fa/652eb2a18fc81358f36c2dd2/image6.png
  [4]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/bltb9458dc116962c7b/652eb2fe74aa53528e325ffc/image4.png
  [5]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blt22ed4f126322aaf9/652eb3460418d27708f75d8b/image5.png
  [6]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blt27df3f81feb75a2a/652eb36e8fc81306dc6c2dda/image10.png
  [7]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blt46ecccd80736dca1/652eb39a701ffe37d839cfd2/image2.png
  [8]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blt1b44f38890b2ea44/652eb3d88dd295fac0efc510/image7.png
  [9]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blt01966229cb4a8966/652eb40148aba383898b1f9a/image11.png
  [10]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/bltf52ce0e04106ac69/652eb46b8d3ed4341e55286b/image3.png
  [11]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blt2b60e8592927d6fa/652eb48e3feebb0b40291c9a/image9.png
  [12]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blt0e97db616428a8ad/652eb4ce8d3ed41c7e55286f/image8.png
  [13]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/bltc35e52e0efa26c03/652eb4fff92b9e5644aa21a4/image1.png