loading...
Cover image for GraphQL + Mongodb. The easy way.

GraphQL + Mongodb. The easy way.

alvarojsnish profile image Álvaro Updated on ・6 min read

EDIT: Seeing the repercussion this post had I'll make one updated, using Prisma, new GraphQL features and will be available for whatever database you want to use. Thanks, Álvaro.
IT'S HERE! New version: https://dev.to/alvarojsnish/graphql-mongo-v2-the-easy-way-6cb

Hi everyone! My name is Alvaro and this is my first post here. I've been writing in other websites such as medium.

BUT! Now I'm here, and I hope to stay here for a while.

I've been playing with GraphQL the last months and, literally, I love it.

Today, we'll learn to:

  1. How to set up a GraphQL server
  2. How to query this API
  3. Connect it to mongo

In the app, we'll have authenticated users, and only that way the will create posts.

Let's start!

1. Set up node with babel

mkdir graphql-test && cd graphql-test
yarn init -y
yarn add --dev nodemon @babel/core @babel/node @babel/preset-env

I'm using yarn, but you can use npm. 
Create a .babelrc file in your root directory, then pase this config:

{
  "presets": ["@babel/preset-env"]
}

2. Create our files and directories organization

  1. In the root, create the folder src
  2. Inside src: models, schemas and resolvers
  3. Now, in src, create index.js
  4. Install all the packages that we will use:
yarn add mongoose jsonwebtoken bcrypt express graphql cors apollo-server apollo-server-express
  1. Create a script in package.json to start the server:
{
  "name": "graphql-test",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "nodemon --exec babel-node src/index.js"
  },
  "devDependencies": {
    "@babel/core": "^7.4.5",
    "@babel/node": "^7.4.5",
    "@babel/preset-env": "^7.4.5",
    "apollo-server": "^2.6.1",
    "apollo-server-express": "^2.6.1",
    "bcrypt": "^3.0.6",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "graphql": "^14.3.1",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^5.5.12",
    "nodemon": "^1.19.1"
  }
}

In index.js is where everything begins.

3. Create the mongo models

Since we wanna focus on GraphQL, lets speed up a bit all the mongo things:

Inside models, create userModel and postModel:

postModel.js

import mongoose from 'mongoose';

const postSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,
  },
  content: {
    type: String,
    required: true,
  },
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'user',
  },
});

const post = mongoose.model('post', postSchema);

export default post;

userModel.js

import bcrypt from 'bcrypt';
import mongoose from 'mongoose';

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  },
  posts: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'post',
    },
  ],
});

userSchema.pre('save', function() {
  const hashedPassword = bcrypt.hashSync(this.password, 12);
  this.password = hashedPassword;
});

const user = mongoose.model('user', userSchema);

export default user;

4. Create our schemas

Inside /src/schemas, we'll create postSchema.js and userSchema.js

import { gql } from 'apollo-server';

export default gql`
  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  extend type Query {
    post(id: ID!): Post!
    posts: [Post!]!
  }

  extend type Mutation {
    createPost(title: String!, content: String!): Post!
  }
`;
import { gql } from 'apollo-server';

export default gql`
  type User {
    id: ID!
    name: String!
    posts: [Post!]!
  }

  type Token {
    token: String!
  }

  extend type Query {
    user(id: ID!): User!
    login(name: String!, password: String!): Token!
  }

  extend type Mutation {
    createUser(name: String!, password: String!): User!
  }
`;
  1. We use the extend anotation because we'll create a linkSchema, to use every schema we add together. We can only have one Query type, so extending it we can use both, also works for mutations and subscriptions.
  2. In the user we don't add the password (security reasons), so the client can't query for it.

This is our link schema:

import userSchema from './user';
import postSchema from './post';
import { gql } from 'apollo-server';

const linkSchema = gql`
  type Query {
    _: Boolean
  }
  type Mutation {
    _: Boolean
  }
`;

export default [linkSchema, userSchema, postSchema];

I created it in schemas/index.js, and it's the schema we'll import later on our index.

5. Create our resolvers

The same way as schemas, we created postResolvers.js and userResolvers.js in src/resolvers

import { AuthenticationError } from 'apollo-server';

export default {
  Query: {
    post: async (parent, { id }, { models: { postModel }, me }, info) => {
      if (!me) {
        throw new AuthenticationError('You are not authenticated');
      }
      const post = await postModel.findById({ _id: id }).exec();
      return post;
    },
    posts: async (parent, args, { models: { postModel }, me }, info) => {
      if (!me) {
        throw new AuthenticationError('You are not authenticated');
      }
      const posts = await postModel.find({ author: me.id }).exec();
      return posts;
    },
  },
  Mutation: {
    createPost: async (parent, { title, content }, { models: { postModel }, me }, info) => {
      if (!me) {
        throw new AuthenticationError('You are not authenticated');
      }
      const post = await postModel.create({ title, content, author: me.id });
      return post;
    },
  },
  Post: {
    author: async ({ author }, args, { models: { userModel } }, info) => {
      const user = await userModel.findById({ _id: author }).exec();
      return user;
    },
  },
};
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { AuthenticationError } from 'apollo-server';

export default {
  Query: {
    user: async (parent, { id }, { models: { userModel }, me }, info) => {
      if (!me) {
        throw new AuthenticationError('You are not authenticated');
      }
      const user = await userModel.findById({ _id: id }).exec();
      return user;
    },
    login: async (parent, { name, password }, { models: { userModel } }, info) => {
      const user = await userModel.findOne({ name }).exec();

      if (!user) {
        throw new AuthenticationError('Invalid credentials');
      }

      const matchPasswords = bcrypt.compareSync(password, user.password);

      if (!matchPasswords) {
        throw new AuthenticationError('Invalid credentials');
      }

      const token = jwt.sign({ id: user.id }, 'riddlemethis', { expiresIn: 24 * 10 * 50 });

      return {
        token,
      };
    },
  },
  Mutation: {
    createUser: async (parent, { name, password }, { models: { userModel } }, info) => {
      const user = await userModel.create({ name, password });
      return user;
    },
  },
  User: {
    posts: async ({ id }, args, { models: { postModel } }, info) => {
      const posts = await postModel.find({ author: id }).exec();
      return posts;
    },
  },
};
  1. Query will resolve all the "functions" we created in our schema, in the type Query.
  2. Mutations, will resolve all the "functions" we created in our schema, in the type Mutation.
  3. User / Post, will resolve a concrete field or type everytime we query the API looking for a user or post. This means that everytime we query for a User > Posts, the server first will go throught the Query > user, and then, will go thought User > posts (posts is the name of the field). We need to do this because we store the data in different collections.

As we see, a resolver is a function, and it has 4 arguments (parent, args, context and info).

parent: will have the data returned from the parent resolver. Example: we go thought Query > user > posts. Posts will have all the data returned un user as a parent argument.

args: will have the arguments we use in the query/mutation. If we see our schemas, post(id: ID!): Post! will have 1 argument, id.

context: the context is an object that will contain everything we pass to it in our server configuration, in our case, it has de mongo models for user and post, and "me", the current user logged in.

info: this is more complex, and Prisma goes in deep here: https://www.prisma.io/blog/graphql-server-basics-demystifying-the-info-argument-in-graphql-resolvers-6f26249f613a

As we did with schemas, create an index.js inside src/resolvers:

import postResolver from './postResolver';
import userResolver from './userResolver';

export default [userResolver, postResolver];

6 Setting everything up

Finally, in our index.js in the src/ folder:

import cors from 'cors';
import express from 'express';
import jwt from 'jsonwebtoken';
import mongoose from 'mongoose';
import { ApolloServer, AuthenticationError } from 'apollo-server-express';

import schemas from './schemas';
import resolvers from './resolvers';

import userModel from './models/userModel';
import postModel from './models/postModel';

const app = express();
app.use(cors());

const getUser = async (req) => {
  const token = req.headers['token'];

  if (token) {
    try {
      return await jwt.verify(token, 'riddlemethis');
    } catch (e) {
      throw new AuthenticationError('Your session expired. Sign in again.');
    }
  }
};

const server = new ApolloServer({
  typeDefs: schemas,
  resolvers,
  context: async ({ req }) => {
    if (req) {
      const me = await getUser(req);

      return {
        me,
        models: {
          userModel,
          postModel,
        },
      };
    }
  },
});

server.applyMiddleware({ app, path: '/graphql' });

app.listen(5000, () => {
  mongoose.connect('mongodb://localhost:27017/graphql');
});
  1. With de function getUser, we are passing the token and verifying it, if the token is not valid, the "me" object will be null, then the client couldn't do request.
  2. When we create the ApolloServer, we pass the schemas as typeDefs, the resolvers as resolvers, and the context, will be an async function that will resolve the previous function we created. Either a user or null, the context will have the mongo models we created, so we can operate with the database in the resolvers.
  3. We add the express server middelware to our app, and set the API Endpoint to /graphql.
  4. We set the port of our app to 5000, then, we connect to the db. Our db will be named graphql.

7. Testing our newborn.

  1. Run "yarn dev" or "npm run dev".
  2. Go to http://localhost:5000/graphql
  3. Make some querys and mutations!

Create user

Login user

Set token in headers

Create posts

Query posts

I hope you enjoyed this as much as I did! You can contact me anytime you want to! And If you want better explanations, just ask for them, I'll be pleased to do it!

Discussion

pic
Editor guide
Collapse
remorses profile image
Tommaso De Rossi

You should check out mongoke, it automatically generates the whole graphql api based on the mongodb database schema, it also handles authorization, relay pagination and more.

Collapse
damiisdandy profile image
damiisdandy

mongoke-sharingan
mind the pun

Collapse
flutterstack profile image
Flutter-Stack

I was trying out this blog . when I was tried using npm to install the packages I got error. yarn is working fine.

apollo-server-core/dist/utils/isDirectiveDefined.js:11
return typeDef.definitions.some(definition => definition.kind === language_1.Kind.DIRECTIVE_DEFINITION &&
^

TypeError: Cannot read property 'some' of undefined

Error throwing at below lines:

const server = new ApolloServer({
typeDefs: schemas,
resolvers,
context: async ({req}) => {
if (req) {
const me = await getUser(req);
return { me, models: { userModel, postModel }}
}
},
});

Collapse
phuocantd_ profile image
Collapse
kaxi1993 profile image
Lasha Kakhidze

Thanks Alvaro

Collapse
kappaxbeta profile image
kappaxbeta

Nice Job :-)

Collapse
kayatechindia profile image
Collapse
kaisheng1 profile image
kaisheng1

Thanks for this really good content! I just have to ask, how would you handle logout though?

Collapse
alvarojsnish profile image
Álvaro Author

Hi, thank you too!

We have many ways to handle logout:

We could do something as Django does and saving the token in the database, then checking it and deleting it when the user wants to logout.

But, I'm more oriented to frontend development, so I would handle this on the frontend, we would store the token in the storage or something then at the time we logout we just delete the token from the storage.

There are a lot of ways of handling this actions and all are valid, you just need to find the one that fits you!

Collapse
saidy_barry profile image
Saidy Barry 🇬🇲

hello, can u help me with the error? Error: Cannot find module './user'

Collapse
alvarojsnish profile image
Álvaro Author

Hi Saidy, maybe you are trying to import a module that doesn’t exists, is in another directory, or has a different name! Check it

Collapse
stronkberg0510 profile image
stronkberg0510

hello. how would i get deleted data from delete mutation. can you help?
tried findOneAndDelete() and it returns null;