GraphQL

Building a GraphQL API Project (Part 1)

18 min Lesson 32 of 35

Building a GraphQL API Project (Part 1)

In this lesson, we'll start building a complete GraphQL API for a blog platform. This practical project will demonstrate schema design, authentication, and CRUD operations in a real-world application.

Project Overview

We'll build a blog platform API with the following features:

  • User authentication and authorization
  • Create, read, update, and delete blog posts
  • Comment system with nested replies
  • User profiles with avatars
  • Post search and filtering
  • Pagination for large datasets

Project Setup

First, let's initialize our Node.js project with the necessary dependencies:

mkdir graphql-blog-api\ncd graphql-blog-api\nnpm init -y\n\n# Install core dependencies\nnpm install apollo-server graphql\n\n# Install database and ORM\nnpm install mongoose\n\n# Install authentication\nnpm install bcryptjs jsonwebtoken\n\n# Install utilities\nnpm install dotenv validator

Project Structure

Create the following directory structure:

graphql-blog-api/\n├── src/\n│   ├── models/           # Database models\n│   │   ├── User.js\n│   │   ├── Post.js\n│   │   └── Comment.js\n│   ├── schema/           # GraphQL schema\n│   │   ├── typeDefs.js\n│   │   └── resolvers.js\n│   ├── utils/            # Utility functions\n│   │   ├── auth.js\n│   │   └── validators.js\n│   ├── context.js        # Context setup\n│   └── index.js          # Server entry point\n├── .env                  # Environment variables\n└── package.json

Database Models

Let's create our MongoDB models using Mongoose:

// src/models/User.js\nconst mongoose = require('mongoose');\n\nconst userSchema = new mongoose.Schema({\n  username: {\n    type: String,\n    required: true,\n    unique: true,\n    trim: true,\n    minlength: 3\n  },\n  email: {\n    type: String,\n    required: true,\n    unique: true,\n    lowercase: true\n  },\n  password: {\n    type: String,\n    required: true,\n    minlength: 6\n  },\n  name: {\n    type: String,\n    required: true\n  },\n  avatar: String,\n  bio: String,\n  createdAt: {\n    type: Date,\n    default: Date.now\n  }\n});\n\nmodule.exports = mongoose.model('User', userSchema);
// src/models/Post.js\nconst mongoose = require('mongoose');\n\nconst postSchema = new mongoose.Schema({\n  title: {\n    type: String,\n    required: true,\n    trim: true\n  },\n  content: {\n    type: String,\n    required: true\n  },\n  excerpt: String,\n  author: {\n    type: mongoose.Schema.Types.ObjectId,\n    ref: 'User',\n    required: true\n  },\n  tags: [String],\n  published: {\n    type: Boolean,\n    default: false\n  },\n  createdAt: {\n    type: Date,\n    default: Date.now\n  },\n  updatedAt: {\n    type: Date,\n    default: Date.now\n  }\n});\n\nmodule.exports = mongoose.model('Post', postSchema);
// src/models/Comment.js\nconst mongoose = require('mongoose');\n\nconst commentSchema = new mongoose.Schema({\n  content: {\n    type: String,\n    required: true\n  },\n  author: {\n    type: mongoose.Schema.Types.ObjectId,\n    ref: 'User',\n    required: true\n  },\n  post: {\n    type: mongoose.Schema.Types.ObjectId,\n    ref: 'Post',\n    required: true\n  },\n  parentComment: {\n    type: mongoose.Schema.Types.ObjectId,\n    ref: 'Comment'\n  },\n  createdAt: {\n    type: Date,\n    default: Date.now\n  }\n});\n\nmodule.exports = mongoose.model('Comment', commentSchema);

GraphQL Schema Design

Now let's define our GraphQL schema with types, queries, and mutations:

// src/schema/typeDefs.js\nconst { gql } = require('apollo-server');\n\nconst typeDefs = gql`\n  type User {\n    id: ID!\n    username: String!\n    email: String!\n    name: String!\n    avatar: String\n    bio: String\n    posts: [Post!]!\n    createdAt: String!\n  }\n\n  type Post {\n    id: ID!\n    title: String!\n    content: String!\n    excerpt: String\n    author: User!\n    tags: [String!]!\n    published: Boolean!\n    comments: [Comment!]!\n    createdAt: String!\n    updatedAt: String!\n  }\n\n  type Comment {\n    id: ID!\n    content: String!\n    author: User!\n    post: Post!\n    parentComment: Comment\n    replies: [Comment!]!\n    createdAt: String!\n  }\n\n  type AuthPayload {\n    token: String!\n    user: User!\n  }\n\n  type Query {\n    # User queries\n    me: User\n    user(id: ID!): User\n    users: [User!]!\n\n    # Post queries\n    post(id: ID!): Post\n    posts(limit: Int, offset: Int): [Post!]!\n    postsByUser(userId: ID!): [Post!]!\n    searchPosts(query: String!): [Post!]!\n\n    # Comment queries\n    comments(postId: ID!): [Comment!]!\n  }\n\n  type Mutation {\n    # Authentication\n    register(username: String!, email: String!, password: String!, name: String!): AuthPayload!\n    login(email: String!, password: String!): AuthPayload!\n\n    # User mutations\n    updateProfile(name: String, avatar: String, bio: String): User!\n\n    # Post mutations\n    createPost(title: String!, content: String!, excerpt: String, tags: [String!]): Post!\n    updatePost(id: ID!, title: String, content: String, excerpt: String, tags: [String!]): Post!\n    deletePost(id: ID!): Boolean!\n    publishPost(id: ID!): Post!\n\n    # Comment mutations\n    createComment(postId: ID!, content: String!, parentCommentId: ID): Comment!\n    deleteComment(id: ID!): Boolean!\n  }\n`;\n\nmodule.exports = typeDefs;
Schema Design Best Practices: Notice how we use non-null types (!) for required fields, separate types for authentication payloads, and include relationships between types. The schema is organized by domain (User, Post, Comment) for clarity.

Authentication Utilities

Create helper functions for authentication and authorization:

// src/utils/auth.js\nconst jwt = require('jsonwebtoken');\nconst bcrypt = require('bcryptjs');\n\nconst JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';\n\n// Generate JWT token\nfunction generateToken(userId) {\n  return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' });\n}\n\n// Verify JWT token\nfunction verifyToken(token) {\n  try {\n    return jwt.verify(token, JWT_SECRET);\n  } catch (error) {\n    return null;\n  }\n}\n\n// Hash password\nasync function hashPassword(password) {\n  return bcrypt.hash(password, 10);\n}\n\n// Compare password\nasync function comparePassword(password, hashedPassword) {\n  return bcrypt.compare(password, hashedPassword);\n}\n\nmodule.exports = {\n  generateToken,\n  verifyToken,\n  hashPassword,\n  comparePassword\n};

Context Setup

Set up the context to include the authenticated user:

// src/context.js\nconst { verifyToken } = require('./utils/auth');\nconst User = require('./models/User');\n\nasync function context({ req }) {\n  // Get token from headers\n  const token = req.headers.authorization?.replace('Bearer ', '');\n  \n  if (!token) {\n    return { user: null };\n  }\n\n  // Verify token and get user\n  const decoded = verifyToken(token);\n  \n  if (!decoded) {\n    return { user: null };\n  }\n\n  try {\n    const user = await User.findById(decoded.userId);\n    return { user };\n  } catch (error) {\n    return { user: null };\n  }\n}\n\nmodule.exports = context;

Basic Resolvers

Let's create the authentication resolvers to get started:

// src/schema/resolvers.js\nconst { AuthenticationError, UserInputError } = require('apollo-server');\nconst User = require('../models/User');\nconst Post = require('../models/Post');\nconst Comment = require('../models/Comment');\nconst { generateToken, hashPassword, comparePassword } = require('../utils/auth');\n\nconst resolvers = {\n  Query: {\n    me: async (_, __, { user }) => {\n      if (!user) {\n        throw new AuthenticationError('Not authenticated');\n      }\n      return user;\n    },\n\n    user: async (_, { id }) => {\n      return User.findById(id);\n    },\n\n    users: async () => {\n      return User.find();\n    }\n  },\n\n  Mutation: {\n    register: async (_, { username, email, password, name }) => {\n      // Check if user already exists\n      const existingUser = await User.findOne({\n        $or: [{ email }, { username }]\n      });\n\n      if (existingUser) {\n        throw new UserInputError('User already exists');\n      }\n\n      // Hash password\n      const hashedPassword = await hashPassword(password);\n\n      // Create user\n      const user = await User.create({\n        username,\n        email,\n        password: hashedPassword,\n        name\n      });\n\n      // Generate token\n      const token = generateToken(user.id);\n\n      return { token, user };\n    },\n\n    login: async (_, { email, password }) => {\n      // Find user\n      const user = await User.findOne({ email });\n\n      if (!user) {\n        throw new AuthenticationError('Invalid credentials');\n      }\n\n      // Verify password\n      const valid = await comparePassword(password, user.password);\n\n      if (!valid) {\n        throw new AuthenticationError('Invalid credentials');\n      }\n\n      // Generate token\n      const token = generateToken(user.id);\n\n      return { token, user };\n    }\n  },\n\n  User: {\n    posts: async (user) => {\n      return Post.find({ author: user.id });\n    }\n  }\n};\n\nmodule.exports = resolvers;
Tip: The resolvers are organized by operation type (Query, Mutation) and include field resolvers for related data. This modular approach makes the code easier to maintain and test.

Environment Configuration

Create a .env file for configuration:

# .env\nMONGODB_URI=mongodb://localhost:27017/graphql-blog\nJWT_SECRET=your-super-secret-jwt-key-change-in-production\nPORT=4000
Warning: Never commit the .env file to version control. Always use strong, unique values for JWT_SECRET in production environments.

Next Steps

In the next lesson, we'll complete the project by:

  • Implementing the remaining post and comment resolvers
  • Adding server setup with Apollo Server
  • Implementing pagination and filtering
  • Adding input validation
  • Testing the complete API
Exercise: Set up the project structure and install all dependencies. Create the database models and review the schema design. Try to predict what the remaining resolvers will look like based on the schema we've defined. In the next lesson, we'll implement them together.