Getting Started
GraphQL, Apollo server and MongoDB all connected on your app.
Dependencies to install
devDependencies are optional, only for the sake of your convenience.
// package.json
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon --exec babel-node src/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"apollo-server-express": "^2.19.0",
"express": "^4.17.1",
"graphql": "^15.4.0",
"mongoose": "^5.10.11"
},
"devDependencies": {
"@babel/cli": "^7.12.1",
"@babel/core": "^7.12.3",
"@babel/node": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"nodemon": "^2.0.6"
}
}
How it works
There are three things to define to use graphQL and the logic might not be specifically applied to MongoDB + graphQL. The logic is simple.
- Let MongoDB how your schemas look like
- Let GraphQL how your schemas look like
- Let Apollo Server how you are going to use these schemas
Logic 1. Defining MongoDB Schema
We are making Transaction schema looking like this:
Transaction {
price
method
cardNumber
paidTime
items: [
{
amount
quantity
}
]
}
We are going to use mongoose as ORM for MongoDB. You just need to define its data type and any additional options if any. It is also very simple. You just define each schema and put them together. MongoDB Schema would look like this:
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const itemSchema = new Schema({
amount: { type: Number },
quantity: { type: Number },
});
const transactionSchema = new Schema({
price: { type: Number, required: true },
method: { type: String, default: 'VISA', required: true },
cardNumber: { type: String, required: true },
paidTime: { type: Date, default: new Date(), required: true },
items: [itemSchema],
});
export const Transaction = mongoose.model('Transaction', transactionSchema);
Break down
- Import mongoose
- Create a schema instance for item (itemSchema)
- Create a schema instance for transaction (transactionSchema)
- put itemSchema into items property of transactionSchema object
Notice itemSchema is going to be part of transactionSchema as an array.
Logic 2. Defining TypeDefs
Let's create a type definition. We are going to use Apollo Server as middleware to handle graphQL. There are other middlewares such as graphql yoga, but Apollo Server is a standard.
Query does things corresponding to GET request, Mutation takes care of any other requests that cause mutation of data such as POST, PUT and DELETE. We are starting by Mutation, because we will push data first, and then fetch them to check if the data properly saved.
There are four type definitions I used in this tutorial:
type SchemaName {types} : Defining schema type
input nameOfInput {types} : Defining schema's input, used to type argument's type
type Query {types}: Defining query structure matching to your resolver
type Mutation {types}: Defining mutation structure matching to your resolver
// typeDefs.js
import { gql } from 'apollo-server-express';
export const typeDefs = gql`
scalar Date
// Defining your Query
// 1 Defining your graphql schema type
type Item {
id: ID!
amount: Float
quantity: Int
}
type Transaction {
id: ID!
price: Float!
method: String!
cardNumber: String!
paidTime: Date!
items: [Item]
}
// 2 Defining input type
input ItemInput {
transactionId: String!
amount: Float
quantity: Int
}
input TransactionInput {
price: Float!
method: String!
cardNumber: String!
items: [ItemInput]
}
// 3 Defining your Muation
type Mutation {
createTransaction(TransactionInput: TransactionInput!): Transaction
createItem(ItemInput: ItemInput): Transaction
`;
Note:! mark means this field is required, notice createItem returns Transaction schema
Break down
- defined Schema of Item and Transaction
- defined type of argument which going to be passed into Mutation function
- defined two functions to create transaction and to create item.
Logic 3. Defining how you are going to create or update api
Resolver is a function to handle populating your data into your database, you can define how you are going to fetch data and how you are going to update, create data. Since Apollo-server reads from schema from typeDefs, they will need to match how it is structured.
Basic Resolver Structure
const resolver = {
Query: {
some async function
},
Mutation: {
some async function
}
}
Let's create resolver file for function and you will need to pass it to the apollo server instance. In the Mutation object, add code like so:
Transaction mutation (parent schema)
import { Transaction } from '../models/transaction';
export const transactionResolver = {
Mutation: {
createTransaction: async (
_, { TransactionInput: { price, method, cardNumber } }
) => {
const newtransaction = new Transaction({
price,
method,
cardNumber,
});
await transaction.save();
return newtransaction;
},
},
}
Firstly, createTransaction is an async function, takes a few arguments but we only care about the second argument which is what we are going to push to the database and its type.
Secondly, create a Transaction instance imported from mongoose model with input arguments (price, method, cardNumber) then save the instance using mongoose.
Finally, return the instance.
Item resolver (child schema)
import { Transaction } from '../models/transaction';
export const itemResolver = {
Mutation: {
createItem: async (
-, {ItemInput: {transactionId, amount, quantity} }
) => {
// find the transaction by id
const transaction = await Transaction.findById(transactionId);
// check if the transaction exists
if (transaction) {
// if exists, push datas into items of transaction
transaction.items.unshift({
amount,
quantity
});
} else throw new Error('transaction does not exist');
Create transaction
Now let's create a transaction! You can open up graphql testing playground at your localserver/graphql
mutation {
functionName(input arguments) {
data you want to return, you can be selective
}
}
Create Items
You can push items into a transaction that you selected with id.
Remember, the transactionId should exist in your database.
Fetching query
// typeDefs.js
import { gql } from 'apollo-server-express';
export const typeDefs = gql`
scalar Date
// Defining your Query
type Query {
transactions: [Transaction!]!
}
// Defining your graphql schema type
type Item {
id: ID!
amount: Float
quantity: Int
}
type Transaction {
id: ID!
price: Float!
method: String!
cardNumber: String!
paidTime: Date!
items: [Item]
}
`;
Type definition of Item Shcema
Type definition of Transaction Schema (notice Item type definition is nested in Transaction type definition in items field)
Create Query type that fetches an array of transaction
Transaction Resolver
import { Transaction } from '../models/transaction';
export const transactionResolver = {
Query: {
transactions: async () => {
try {
const transactions = await Transaction.find()
return transactions;
} catch (error) {
throw new Error(error);
}
},
},
Mutation: { mutation code ... }
}
Define an async function corresponding to the one in our typeDefs. We defined transactions in Query type in our typeDefs like so:
// typeDef.js - our Query type
type Query {
transactions: [Transaction!]!
}
Now let's fetch data in our localhost:port/graphql. Easy peasy. Isn't it? If you are querying data, you can omit query at the beginning of the object as the image below.
query {
transactions {
id
method
cardNumber
PadTime
items {
id
amount
quantity
}
}
}
Conclusion
Nesting schema is easy, however, we need to be precise how we want it to be. If things are not working check whether your schema field's names are matching with the one in your resolver and its structure.
Top comments (0)