Building a GraphQL API Project (Part 1)
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;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;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
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