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.
You'll then be asked to create your first 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:
Once the Data API has been enabled, you should see your 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:
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")
}