loading...
Cover image for GraphQL + Mongo v2. The easy way.

GraphQL + Mongo v2. The easy way.

alvarojsnish profile image Álvaro ・6 min read

Hi everyone, it's Álvaro again.

This post it's the updated version of: https://dev.to/alvarojsnish/graphql-mongodb-the-easy-way-ngc, but today we are adding a new friend to our stack: Prisma! and we'll change something from it, we'll use GraphQL-Yoga instead of Apollo.

Why prisma?
Because we love our time and prisma reduces our time typing thousand of lines of code to access to our data, provides us a beautiful visual data manager and simplifies everything a lot.

Enough talk, let's start. Everything right now is the same as the old post:

You can get the full code here: Github repo

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: middleware, schemas and resolvers
  3. Now, in src, create index.js
  4. Install all the packages that we will use:
yarn add dotenv jsonwebtoken bcrypt graphql-yoga graphql-resolvers prisma-client-lib

3. Set up Prisma

  • In the root, create the folder prisma
  • Install globally prisma:
npm install -g prisma
  • Install docker and run it (prisma needs docker locally)
  • Enter the prisma folder and create a docker-compose to download and install the prisma img and the database (mongo ftw). I'll show you how to secure your database, in the prisma docs is a little confuse.
touch docker-compose.yml (or create the file)

Paste this configuration

version: '3'
services:
  prisma:
    image: prismagraphql/prisma:1.34
    restart: always
    ports:
      - '${PRISMA_PORT}:${PRISMA_PORT}'
    environment:
      PRISMA_CONFIG: |
        managementApiSecret: ${PRISMA_MANAGEMENT_API_SECRET}
        port: ${PRISMA_PORT}
        databases:
          default:
            connector: ${PRISMA_DB_CONNECTOR}
            uri: ${PRISMA_DB_URI}
  mongo:
    image: mongo:3.6
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
    ports:
      - '${MONGO_PORT}:${MONGO_PORT}'
    volumes:
      - mongo:/var/lib/mongo
volumes:
  mongo: ~

As you can see, the important data is hidden, we need to create .env file that will load this data for us (I'll put some random but valid data, the PORTS are the default for Prisma):

PRISMA_PORT=4466
PRISMA_MANAGEMENT_API_SECRET=7a7a96a9-0c65-48b6-96bf-5a4f03e2076c
PRISMA_DB_CONNECTOR=mongo
PRISMA_DB_URI=mongodb://prisma:prisma@mongo
MONGO_INITDB_ROOT_USERNAME=mongo
MONGO_INITDB_ROOT_PASSWORD=prisma
MONGO_PORT=27017

It's SUPER recomendable to use your own data.

  • Run:
docker-compose up -d

And, if everything goes okay, prisma and mongo will be running!

  • Initialize prisma:
prisma init --endpoint http://localhost:YOUR_PORT

This will generate our files datamodel.prisma and prisma.yml

  • Since we are using mongo (the database are documents) and we secured our docker image, we need to make some changes in the prisma.yml:
endpoint: http://localhost:YOUR_PORT
datamodel: datamodel.prisma
databaseType: document
secret: ${env:PRISMA_MANAGEMENT_API_SECRET}

generate:
    - generator: javascript-client
      output: ./generated/prisma-client/

hooks:
  post-deploy:
    - prisma generate
  • Create a sample schema in datamodel.prisma:
type User {
  id: ID! @id
  password: String!
  email: String! @unique
}
  • Run the deploy command:
prisma deploy

You can manage the data on the manager we talked about at the beginning:
http://localhost:YOUR_PORT/_admin

You will need a token to access it, you can generate it with

prisma token

and configure it on the manager. (I almost never used the manager tbh).

  • On our prisma.yml we configured a post-deploy hook to generate our prisma bin files, every time we change our datamodel, we need to deploy prisma and generate the files. The generation is automatic with the deploy, but if you want to do it manually:
prisma deploy
prisma generate

This will make the javascript client in ./generated/prisma-client

That's everything for prisma, let's move on the GraphQL and Javascript part!

Create a .env in the root and paste your variables:

PORT=4400
JWT_SECRET_KEY=cd72be3f-4f62-47ad-8e46-610bc2f40219
PRISMA_SECRET=7a7a96a9-0c65-48b6-96bf-5a4f03e2076c

Now, create an index.js inside the 'src' folder, and create the server:

import { GraphQLServer } from 'graphql-yoga';
import { importSchema } from 'graphql-import';

import resolvers from './resolvers';
import { Prisma } from '../prisma/generated/prisma-client';

import 'dotenv/config';

const typeDefs = importSchema('src/schemas/user.graphql');

export const db = new Prisma({
  endpoint: process.env.PRISMA_ENDPOINT || 'http://localhost:4466',
  secret: process.env.PRISMA_SECRET || '',
});

const server = new GraphQLServer({
    typeDefs,
    resolvers,
    context: async () => ({
        prisma: db,
    }),
});

server.start({ port: process.env.PORT }, () => {
    console.log('App running on http://localhost:4400');
});

Don't worry about the unresolved imports now, we are going to create them soon.
2 things to note here:

  • The Prisma initialization: we need to set our key to make the communication between the prisma-client and the server.
  • The prisma object we are passing to the GraphQL context. This is everything.

Add this script to your package.json:

"scripts": {
    "dev": "nodemon --exec babel-node ./src/index.js"
 },

and run the server (it will crash hardly because the unresolved imports, don't worry)

yarn dev
npm run dev

Let's start with our schema, create inside src, a folder called schemas, then the file user.graphql inside:

type User {
    id: ID!
    email: String!
}

type Token {
    token: String!
}

type Query {
    signIn(email: String!, password: String!): Token!
    getUser(id: ID!): User!
}

type Mutation {
    signUp(email: String!, password: String!): Token
}

Note that we don't put the password in the User type, Prisma will handle it with the database, if we put it here, anyone could query for it, and we don't want that!

  • Now, let's create our resolver. Create a folder resolvers, and inside put an index.js with:
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';

export default {
    Query: {
        getUser: async (parent, { id }, { prisma }) => {
            const user = await prisma.user({ id });
            return user;
        },
        signIn: async (parent, { email, password }, { prisma }) => {
            try {
                const user = await prisma.user({ email });

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

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

                if (!passwordMatch) {
                    throw new Error('Invalid credentials');
                }

                const token = jwt.sign({ user }, process.env.JWT_SECRET_KEY, { expiresIn: 36000 });

                return { token };
            } catch (error) {
                throw new Error(error);
            }
        }
    },
    Mutation: {
        signUp: async (parent, { email, password }, { prisma }) => {
            try {
                const hashedPassword = bcrypt.hashSync(password, 12);
                const user = await prisma.createUser({ email, password: hashedPassword });

                const token = jwt.sign({ user }, process.env.JWT_SECRET_KEY, { expiresIn: 36000 });

                return { token };
            } catch (error) {
                throw new Error(error);
            }
        }
    }
}

Now, let's try our signUp! Navigate to localhost:4400 and try the signUp mutation, it will give us a token! (not so useful yet I guess)

Let's try to signIn with our user with the query signIn:

Another token, huh, what can we do with it? Hmmm, we had a query to get a user right? Let's make that only authenticated users can query it!

Modify the schema, add "me":

type Query {
    signIn(email: String!, password: String!): Token!
    getUser(id: ID!): User!
    me: User!
}

Now, let's add the generated token to our header:

  • Go to the playground and paste in down there, on http headers:

We need to get that token, it contains our user info... but how?
Hmm... maybe the context can do something!
Modify the index.js on src to this:

import { GraphQLServer } from 'graphql-yoga';
import { importSchema } from 'graphql-import';
import jwt from 'jsonwebtoken';

import resolvers from './resolvers';
import { Prisma } from '../prisma/generated/prisma-client';

import 'dotenv/config';

const typeDefs = importSchema('src/schemas/user.graphql');

export const db = new Prisma({
  endpoint: process.env.PRISMA_ENDPOINT || 'http://localhost:4466',
  secret: process.env.PRISMA_SECRET || '',
});

const getCurrentUser = async (request) => {
    if (!request.headers.token) {
        return null;
    }
    const user = await jwt.decode(
        request.headers.token,
        process.env.JWT_SECRET_KEY,
    );
    return { ...user };
};

const server = new GraphQLServer({
    typeDefs,
    resolvers,
    context: async ({ request }) => {
        const me = await getCurrentUser(request);

        return {
          me,
          prisma: db,
        };
    }
});

server.start({ port: process.env.PORT }, () => {
    console.log('App running on http://localhost:4400');
});

We are almost there, let's add the resolver for "me":

Query: {
        me: async (parent, { id }, { prisma, me }) => {
            const user = await prisma.user({ id: me.user.id });
            return user;
        },
...

If we try to query "me", we'll get...

That means that we are authenticated, "me" is someone, and the token is working. How can we use this power? We installed with yarn a package named combine-resolvers, so let's create a resolver to make auth requests:

Inside the index.js in resolvers:
Import the combine-resolvers:

import { combineResolvers, skip } from 'graphql-resolvers';

Create the resolver:

  • If "me" exists, we skip to the next resolver, the "getUser", if not exists, we throw an error.
const userIsAuthenticated = (parent, args, { me }) => {
    return me ? skip : new Error('Not authenticated');
}

And combine the getUser:

getUser: combineResolvers(
    userIsAuthenticated,
        async (parent, { id }, { prisma }) => {
            const user = await prisma.user({ id });
            return user;
        }
),

Save and let's try our resolver:

PS: if you are wondering where did I get the ID to query the getUser, just query "me" and ask for the ID instead the email (you can query both, you can query for everything you put in your schema (not prisma schema).).

  • With token

  • Without token

So, we reached our final power. We learned how to authenticate and authorize with a GraphQL server powered with Prisma.

Now, you can do everything you can imagine, roles authorization, permissions, etc. Very simple but very powerful.

As always, thank you for reaching here, and if you have any kind of problem, just let me know, I'll help you in every way I can.

Again, Álvaro here, I hope you enjoyed this!
Get the code here: Github repo

Posted on by:

alvarojsnish profile

Álvaro

@alvarojsnish

I'm a young spanish developer and React / GraphQL enthusiast, interested in AI/DL, python ecosystem and self-drived devices.

Discussion

pic
Editor guide
 

Hi. This is a great post. Though I am curious from looking at the repo how prisma is using the mongo server? There doesn't seem to be any configuration pointing to it. Also given prisma latest on mongo support is that it isn't supported yet.
github.com/prisma/prisma/issues/12...

Can you provide documentation explaining what is supported, and how mongo is actually connected to via prisma?

 

Hi Steve, that post is related to prisma 2 as I know. Here you can find the answer to your question: v1.prisma.io/docs/1.34/get-started.... All the information you need is written on the post. You should try Prisma 2 even though they still don't have the mongodb driver ready, but the work they are doing with Prisma Migrate is awesome!
instructions

 

What did Prisma actually do for us? We still wrote all the resolvers and queries and the schema? Does it allow us to connect to any kind of database? Trying to understand how to link graphQL into a persistent database. Thanks!

 

Prisma is playing as our ORM, making the transactions with the database. It does a lot of things in the background, solves for us the n+1 problem, does all the CRUD boilerplate operations, etc, you only need to call that "prisma" object with the table you want to query, in our case it's user(s).

But we still need to write our schemas and resolvers, of course, because that's how GraphQL works.

The "kind" of database that it allows to connect, it's defined on our docker-compose, in this case it's mongodb, but you can choose your database. We don't install anything like mongodb on our computer (mysql or postgres), docker does everything for us.

Here you have more info:
prisma.io/docs/get-started/01-sett...

And you can even start from an existing database:
prisma.io/docs/get-started/01-sett...

If you still want don't want to write resolvers or schemas, hasura simplifies this even more: hasura.io/

But, in the end, you need to code both.