Working with GraphQL, MongoDB Data API and Edge Resolvers

Working with GraphQL, MongoDB Data API and Edge Resolvers

Resolvers are a powerful way to extend your Grafbase backend. We can use resolvers to compute custom business logic, make network requests, invoke dependencies, and lots more.

MongoDB is a highly adaptable document database that boasts a vast array of versatile querying and indexing functionalities.

The MongoDB Atlas Data API is an innovative interface built on top of your MongoDB database, tailored to excel in serverless environments, like the edge. Operating over HTTP, this API can be seamlessly integrated using the fetch API within a resolver.

In this guide, we'll create a custom GraphQL query and mutation using Edge Resolvers that finds and creates MongoDB documents using the Data API.

If you've not seen the MongoDB Data API before then the cURL below should give you a good idea of what's possible:

curl --request POST \ 'https://data.mongodb-api.com/app/data-abcde/endpoint/data/v1/action/insertOne' \ --header 'apiKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "dataSource": "Cluster0", "database": "my-first-database", "collection": "products", "document": { "name": "Sunglasses", } }'

You will need to create an account with MongoDB Atlas.

Once logged in, create a database from the Database Deployments overview.

For the purposes of this guide, use these options:

  • Plan/Cluster: M0
  • Provider: AWS
  • Region: eu-central-1
  • Name: Cluster0

Once your database cluster has been created, go to browse collections from the overview page and proceed to Add My Own Data.

Add My Own Data

You'll then be asked to create your first database and collection:

Create database and collection

You will now be redirected to the Collections view for your newly created database and collection.

We'll be using Grafbase to insert and find data from our collection.

Now we've a cluster, database and collection, we can enable the Data API.

Select Data API from the Atlas sidebar and select the data source we created earlier:

Enable Data API

Once the Data API has been enabled, you should see your URL Endpoint:

URL Endpoint

Save your URL Endpoint to a safe place. We'll need this later.

Now click Create API Key and give it a name:

Create API Key

Save your API Key to a safe place. We'll need this next.

Inside a new directory or an existing project run the following command:

npx grafbase init

We'll be using MongoDB to save our products so we will need to create a GraphQL type that represents our Product document.

Inside grafbase/schema.graphql add the following:

type Product { id: ID! name: String! price: Int! }

Finally, create the file grafbase/.env and add the values for the Data API you saved from the step above:

MONGODB_DATA_API_URL= MONGODB_DATA_API_KEY= MONGODB_DATA_SOURCE= MONGODB_DATABASE= MONGODB_COLLECTION=

We can use the Data API action /insertOne to insert a new product.

To do this we will need to add the custom createProduct mutation inside grafbase/schema.graphql. We will use the special directive @resolver which tells Grafbase which resolver function to execute.

We will also create a custom input type for CreateProductInput that will be used for the input argument:

extend type Mutation { createProduct(input: CreateProductInput!): Product @resolver(name: "create-product") } input CreateProductInput { name: String! price: Int! }

Now create the file grafbase/resolvers/create-product.ts and add the following:

const baseUrl = process.env.MONGODB_DATA_API_URL const apiKey = process.env.MONGODB_DATA_API_KEY export default async function CreateProductResolver(_, { input }) { const { name, price } = input try { // ... } catch (err) { return null } }

The Data API expects a JSON payload that includes details about the dataSource, database, collection and the document itself.

The JSON object we will send will look something like this:

{ "dataSource": "Cluster0", "database": "my-first-database", "collection": "products", "document": { "name": "...", "price": "..." } }

The value for name and price we already have from our input argument.

We'll also assign the environment variables into the const dataSource, database and collection that we will re-use throughout the different resolvers.

If we put all of this together with our headers it should look something like this:

const dataSource = process.env.MONGODB_DATA_SOURCE const database = process.env.MONGODB_DATABASE const collection = process.env.MONGODB_COLLECTION export default async function CreateProductResolver(_, { input }) { const { name, price } = input try { const response = await fetch(`${baseUrl}/action/insertOne`, { method: 'POST', headers: { 'content-type': 'application/json', 'api-key': apiKey, }, body: JSON.stringify({ dataSource, database, collection, document: { name, price, }, }), }) const data = await response.json() return { id: data.insertedId, name, price, } } catch (err) { return null } }

If you want to add more data to your document, make sure to update the input type and body of the request to the Data API.

Now start the Grafbase development server using the CLI:

npx grafbase dev

You're now ready to test it out!

Go to http://localhost:4000 and execute the following mutation:

mutation { createProduct(input: { name: "Sunglasses", price: 100 }) { id name price } }

You will get a response that contains the product data as well as the id which is returned from MongoDB as the insertedId:

{ "data": { "createProduct": { "id": "64526f53906b0e0d83c09844", "name": "Sunglasses", "price": 100 } } }

Now we've some data inside our MongoDB database, we can now create a custom GraphQL query to find all documents.

Inside grafbase/schema.graphql add the following:

extend type Query { products(limit: Int = 5): [Product] @resolver(name: "products") }

Then create the file grafbase/resolvers/products.ts and add the following:

const baseUrl = process.env.MONGODB_DATA_API_URL const apiKey = process.env.MONGODB_DATA_API_KEY const dataSource = process.env.MONGODB_DATA_SOURCE const database = process.env.MONGODB_DATABASE const collection = process.env.MONGODB_COLLECTION export default async function ProductsResolver(_, { limit }) { try { const response = await fetch(`${baseUrl}/action/find`, { method: 'POST', headers: { 'content-type': 'application/json', 'api-key': apiKey, }, body: JSON.stringify({ dataSource, database, collection, limit, }), }) const data = await response.json() return data?.documents?.map(({ _id: id, name, price }) => ({ id, name, price, })) } catch (err) { return null } }

The above resolver has the argument limit which has a default value set inside the grafbase/schema.graphql that is 5.

This limit is passed onto the find action so documents are limited from MongoDB.

Let's try it out! Go to http://localhost:4000 and execute the following query:

query { products(limit: 1) { id name price } }

We can now create the types and resolver for fetching a single product by ID.

Inside grafase/schema.graphql add the new query product:

extend type Query { # ... product(id: ID!): Product @resolver(name: "product") }

Then create the file grafbase/resolvers/product.ts and add the following:

const baseUrl = process.env.MONGODB_DATA_API_URL const apiKey = process.env.MONGODB_DATA_API_KEY const dataSource = process.env.MONGODB_DATA_SOURCE const database = process.env.MONGODB_DATABASE const collection = process.env.MONGODB_COLLECTION export default async function ProductResolver(_, { id }) { try { const response = await fetch(`${baseUrl}/action/findOne`, { method: 'POST', headers: { 'content-type': 'application/json', 'api-key': apiKey, }, body: JSON.stringify({ dataSource, database, collection, filter: { _id: { $oid: id, }, }, }), }) const { document } = await response.json() if (document === null) return null return { id, ...document, } } catch (err) { return null } }

Give it a try:

query { product(id: "64526d779096d402ba87395a") { id name price } }

We can now create the types and resolver for updating a single product by ID.

Inside grafase/schema.graphql add the new mutation updateProduct:

extend type Mutation { # ... updateProduct(id: ID!, input: UpdateProductInput!): Product @resolver(name: "update-product") } input UpdateProductInput { name: String price: Int }

Then create the file grafbase/resolvers/update-product.ts and add the following:

const baseUrl = process.env.MONGODB_DATA_API_URL const apiKey = process.env.MONGODB_DATA_API_KEY const dataSource = process.env.MONGODB_DATA_SOURCE const database = process.env.MONGODB_DATABASE const collection = process.env.MONGODB_COLLECTION export default async function UpdateProductResolver(_, { id, input }) { const { name, price } = input try { const response = await fetch(`${baseUrl}/action/findOne`, { method: 'POST', headers: { 'content-type': 'application/json', 'api-key': apiKey, }, body: JSON.stringify({ dataSource, database, collection, filter: { _id: { $oid: id, }, }, }), }) const { document } = await response.json() if (document === null) return null await fetch(`${baseUrl}/action/updateOne`, { method: 'POST', headers: { 'content-type': 'application/json', 'api-key': apiKey, }, body: JSON.stringify({ dataSource, database, collection, filter: { _id: { $oid: id, }, }, update: { $set: { name, price, }, }, }), }) return { id, ...document, ...input, } } catch (err) { return null } }

Give it a try:

mutation { updateProduct(id: "64526d779096d402ba87395a", input: { name: "Hat" }) { id name price } }

We can now create the types and resolver for deleting a single product by ID.

Inside grafase/schema.graphql add the new mutation deleteProduct:

extend type Mutation { # ... deleteProduct(id: ID!): Boolean @resolver(name: "delete-product") }

Then create the file grafbase/resolvers/delete-product.ts and add the following:

const baseUrl = process.env.MONGODB_DATA_API_URL const apiKey = process.env.MONGODB_DATA_API_KEY const dataSource = process.env.MONGODB_DATA_SOURCE const database = process.env.MONGODB_DATABASE const collection = process.env.MONGODB_COLLECTION export default async function DeleteProductResolver(_, { id }) { try { const response = await fetch(`${baseUrl}/action/deleteOne`, { method: 'POST', headers: { 'content-type': 'application/json', 'api-key': apiKey, }, body: JSON.stringify({ dataSource, database, collection, filter: { _id: { $oid: id, }, }, }), }) const { deletedCount } = await response.json() return !!deletedCount } catch (err) { return false } }

Give it a try:

mutation { deleteProduct(id: "64526d779096d402ba87395a") }

Get Started

Build your API of the future now.