NodeJS Graphql Server with Apollo
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 :
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]
We can’t resolve a wrong typeDefs :
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 :
Launching server
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 :
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