NodeJS Graphql Server with Apollo

Héla Ben Khalfallah
7 min readJun 16, 2020

NodeJS, Apollo Graphql Server, Mongoose, Mongo DB

Project setup

Init project

mkdir node-graphql-apicd node-graphql-apinpm init --yesnpm install mongoosenpm install apollo-server graphqlnpm install nodemon dotenv @babel/core @babel/node @babel/plugin-transform-async-to-generator @babel/plugin-transform-runtime @babel/preset-env --save-dev

package.json

"start": "nodemon --exec babel-node index.js",

Babel Config

touch babel.config.js

babel.config.js :

module.exports = function (api) {
api.cache(true);

const presets = [
"@babel/preset-env"
];
const plugins = [
"@babel/plugin-transform-async-to-generator",
"@babel/plugin-transform-regenerator",
["@babel/plugin-transform-runtime", {
"helpers": true,
"regenerator": true
}]
];
return {
presets,
plugins
};
}

dotenv (.env)

touch .env

.env :

DEFAULT_LANGUAGE = en
GRAPHQL_APP_PORT = 5000
GRAPHQL_APP_PATH = /graphql

MONGOOSE_DB_HOST = 127.0.0.1
MONGOOSE_DB_PORT = 27017
MONGOOSE_DB_NAME = local

index.js

touch index.js

index.js

//dot env configuration
var dotenv = require('dotenv')
dotenv.config()

//launch server after loading env var
require('./server/server.js')

Start mongo db

mongod --dbpath ./mongodata

Apollo graphql server

Setup

npm install apollo-server graphqlmkdir server
cd server
touch server.js

Server init

import { ApolloServer, gql } from 'apollo-server';

const typeDefs = gql`
`;

const resolvers = {};

const server = new ApolloServer({ typeDefs, resolvers });

// The `listen` method launches a web server.
const portNumber = process.env.GRAPHQL_APP_PORT || 4000
server.listen(portNumber).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

TypeDefs

Type Defs are like .h files for C programming language. They define :

  • models schemas
  • get queries definition (only definition)
  • crud mutations definition (only definition)

TypeDefs are used to document graphql apis :

apollo playground — graphql documentation

Schema

User schema :

const UserType = `
type UserType {
id: ID!
firstName: String!
lastName: String!
birthday: DateTime
phone: String
email: String!
username: String!
password: String!
posts: [PostType]
isConnected: Boolean!
numberOfFollowers: Int!
}
`

export default UserType;

Post :

const PostType = `
type PostType {
id: ID!
text: String!
user: String!
createdAt: DateTime!
}
`

export default PostType;

Queries

const UserTypeQueries = `
type Query {
users: [UserType]
user(email: String!): UserType
posts(email: String!): PostType
}
`

export default UserTypeQueries;

Mutations

const UserTypeMutations = `
type Mutation {
addUser(firstName: String!, lastName: String!, birthday: DateTime, phone: String, email: String!, username: String!, password: String!): UserType
updateUser(firstName: String!, lastName: String!, birthday: DateTime, phone: String, username: String!, email: String!): UserType
deleteUser(email: String!): UserType
addPost(text: String!, user: String!): PostType
}
`

export default UserTypeMutations;

Resolvers

Resolvers are like .m files for C language. They contain the implementation for each query or mutation. They respond to the question : How to query or mutate data ?

import {
UserProvider,
PostProvider,
} from '../../database';

const {
getUsers,
userByEmail,
addUser,
updateUser,
} = UserProvider;

const {
addPost,
} = PostProvider;

const UserResolvers = {
Query: {
users: () => getUsers(),
user: (_, { email }, context) => userByEmail(_, email, context),
},

Mutation: {
addUser: (_, params, context) => addUser(_, params, context),
updateUser: (_, params, context) => updateUser(_, params, context),
deleteUser: (_, { email }, context) => null,
addPost: (_, params, context) => addPost(_, params, context),
}
};

export default UserResolvers;

We can’t define a resolver without its TypeDefs :

Error during db init :  [Error: Query.userById defined in resolvers, but not in schema]
graphql validation

We can’t resolve a wrong typeDefs :

field not exist in schema
unknown argument
unknown argument
missing required argument
missing required argument

Data Provider

Mongo db connection

import mongoose from 'mongoose';

//promise
mongoose.Promise = global.Promise;

//connect
const DBConnect = async () => {
const dbHost = process.env.MONGOOSE_DB_HOST;
const dbPort = process.env.MONGOOSE_DB_PORT;
const dbName = process.env.MONGOOSE_DB_NAME;
const dbUrl = `mongodb://${dbHost}:${dbPort}/${dbName}`;
try {
await mongoose.connect(dbUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
});
console.log('🚀 Connected to mongo!!!');
}
catch (err) {
console.log('Could not connect to MongoDB');
}
}

export default DBConnect;

Server connection

import { ApolloServer, gql } from 'apollo-server';
import { GraphQLDateTime } from 'graphql-iso-date';
import {
UserType,
PostType,
UserTypeQueries,
UserTypeMutations,
UserResolvers,
}from '../users';
import {
DBConnect,
}from '../database';

const typeDefs = gql`
# custom type
scalar DateTime

# models
${UserType}
${PostType}

# the schema allows the following query:
${UserTypeQueries}

# this schema allows the following mutation:
${UserTypeMutations}
`;

const resolvers = {
DateTime: GraphQLDateTime,
...UserResolvers,
};

DBConnect
.then
(() => {
// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
// https://github.com/the-road-to-graphql/fullstack-apollo-express-mongodb-boilerplate/blob/master/src/index.js#L54
const server = new ApolloServer({ typeDefs, resolvers });

// The `listen` method launches a web server.
const portNumber = process.env.GRAPHQL_APP_PORT || 4000
server.listen(portNumber).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
})
.catch((error) => {
console.log('an error occured while starting DB : ', error);
});

UserProvider

import User from '../schemas/User';
import PostProvider from './PostProvider';

const {
getPostsByUser,
} = PostProvider;

const getUsers = async () => {
try {
const users = await User.find().exec();
if(users && users.length){
users.forEach(user => {
const {
email,
} = user;
user.posts = getPostsByUser(email);
});
}
return users;
} catch (error) {
throw new Error(error);
}
};

const userByEmail = async (_, email, context) => {
try {
const user = await User.findOne({ email: email }).exec();
if(user){
const {
email,
} = user;
user.posts = getPostsByUser(email);
}
return user;
} catch (error) {
throw new Error(error);
}
};

const addUser = async (_, params, context) => {
try {
const {
firstName,
lastName,
birthday,
email,
phone,
username,
password,
} = params || {};
if(firstName && firstName.length
&& lastName && lastName.length
&& username && username.length
&& email && email.length
&& password && password.length){
const user = await User.findOne({ email: email }).exec();
if(user){
throw new Error('User already exist !');
}
const userModel = new User({
firstName,
lastName,
birthday,
email,
phone,
username,
password,
});
return await userModel.save();
}
throw new Error('lastName, lastName, username, email and password are required field !');
} catch (error) {
throw new Error(error);
}
}

const updateUser = async (_, params, context) => {
try {
const {
firstName,
lastName,
birthday,
email,
phone,
username,
} = params || {};
if(firstName && firstName.length
&& lastName && lastName.length
&& username && username.length
&& email && email.length){
const user = await User.findOne({ email: email }).exec();
if(!user){
throw new Error('User not exist !');
}
user.firstName = firstName;
user.lastName = lastName;
user.birthday = birthday;
user.phone = phone;
user.username = username;
return await user.save();
}
throw new Error('lastName, lastName, username and email are required field !');
} catch (error) {
throw new Error(error);
}
}

const UserProvider = {
getUsers,
userByEmail,
addUser,
updateUser,
};

export default UserProvider;

Post Provider

import Post from '../schemas/Post';

const addPost = async(_, params, context) => {
try {
const {
text,
user,
} = params || {};
if(text && text.length
&& user && user.length){
const postModel = new Post({
text,
user,
});
return await postModel.save();
}
throw new Error('user and text are required field !');
} catch (error) {
throw new Error(error);
}
}

const getPostsByUser = async (email) => {
try {
const posts = await Post.find({ user: email }).exec();
return posts;
} catch (error) {
throw new Error(error);
}
}

const PostProvider = {
getPostsByUser,
addPost,
};

export default PostProvider;

Communication is like this :

communication flow

Launching server

get all users
add user

The advantage about graphql queries is that we can choose what we want.

Single server but client is free to pick what he want instead of having multiple Rest Queries !

How a graphql request looks like ?

A graphql request is a post request where in it body we can find our query or mutation :

graphql request
graphql query
graphql query

Project source code

Graphql client

Thank you for reading my story.

You can find me at :

Twitter : https://twitter.com/b_k_hela

Github : https://github.com/helabenkhalfallah

--

--

Héla Ben Khalfallah

I love coding whatever the language and trying new programming tendencies. I have a special love to JS (ES6+), functional programming, clean code & tech-books.