Blog API with GraphQL

Full-Featured Blogging Platform with Rich Content Management

πŸ“ Project Overview

A comprehensive Blog API built with GraphQL that provides full content management capabilities including posts, categories, tags, comments, and user management. Features rich text editing, image uploads, SEO optimization, and advanced filtering.

🎯 Key Objectives

  • Content Management: Full CRUD operations for posts, categories, and tags
  • Rich Text Support: Markdown/HTML content with syntax highlighting
  • User System: Multi-role authentication (Admin, Editor, Author, Reader)
  • Social Features: Comments, likes, shares, and user profiles
  • SEO Optimized: Meta tags, sitemap generation, and structured data
  • Media Management: Image uploads with automatic optimization

πŸ—οΈ System Architecture

Client Applications

Web, Mobile, Admin Panel

GraphQL API

Apollo Server

Business Logic

Resolvers & Services

Auth Service

JWT & Permissions

Media Service

Image Processing

PostgreSQL

Main Database

Redis

Cache Layer

S3

Media Storage

30+
GraphQL Types
15
Query Operations
20+
Mutations
4
User Roles

✨ Key Features

Comprehensive blogging platform with modern content management capabilities.

πŸ“„

Post Management

Create, edit, and publish posts with rich text content, featured images, excerpts, and scheduling. Support for drafts, published, and archived states.

🏷️

Categories & Tags

Organize content with hierarchical categories and flexible tagging system. Create custom taxonomies for better content organization.

πŸ’¬

Comment System

Nested comment threads with moderation capabilities. Support for likes, spam filtering, and email notifications for comment replies.

πŸ‘₯

User Management

Multi-role system with Admin, Editor, Author, and Reader roles. Profile management, avatar uploads, and activity tracking.

πŸ”

Advanced Search

Full-text search with filters for categories, tags, authors, and date ranges. Relevance scoring and search suggestions.

πŸ“Š

Analytics

Track post views, engagement metrics, popular content, and user activity. Integration with Google Analytics.

🎨

Media Library

Upload and manage images with automatic thumbnail generation, compression, and CDN integration for fast delivery.

🌐

SEO Optimization

Automatic meta tags, Open Graph, Twitter Cards, XML sitemap generation, and structured data for better search rankings.

πŸ“±

API-First Design

Headless CMS approach allows building websites, mobile apps, and custom frontends using the same GraphQL API.

πŸ“ GraphQL Schema

Complete type definitions for the blog API including posts, users, comments, and media.

Core Types

type Post {
  id: ID!
  title: String!
  slug: String!
  content: String!
  excerpt: String
  featuredImage: Image
  status: PostStatus!
  author: User!
  category: Category
  tags: [Tag!]!
  comments: [Comment!]!
  commentCount: Int!
  likes: Int!
  views: Int!
  readingTime: Int!
  publishedAt: DateTime
  createdAt: DateTime!
  updatedAt: DateTime!
  seo: SEOMetadata
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
  SCHEDULED
}

type Category {
  id: ID!
  name: String!
  slug: String!
  description: String
  parent: Category
  children: [Category!]!
  posts: [Post!]!
  postCount: Int!
  image: Image
  createdAt: DateTime!
}

type Tag {
  id: ID!
  name: String!
  slug: String!
  posts: [Post!]!
  postCount: Int!
  createdAt: DateTime!
}

type Comment {
  id: ID!
  content: String!
  author: User!
  post: Post!
  parent: Comment
  replies: [Comment!]!
  status: CommentStatus!
  likes: Int!
  createdAt: DateTime!
  updatedAt: DateTime
}

enum CommentStatus {
  PENDING
  APPROVED
  SPAM
  TRASH
}

type User {
  id: ID!
  email: String!
  username: String!
  firstName: String
  lastName: String
  fullName: String!
  bio: String
  avatar: Image
  role: UserRole!
  posts: [Post!]!
  comments: [Comment!]!
  followers: [User!]!
  following: [User!]!
  followerCount: Int!
  followingCount: Int!
  website: String
  socialLinks: SocialLinks
  createdAt: DateTime!
}

enum UserRole {
  ADMIN
  EDITOR
  AUTHOR
  READER
}

type SocialLinks {
  twitter: String
  facebook: String
  instagram: String
  linkedin: String
  github: String
}

type Image {
  id: ID!
  url: String!
  thumbnail: String
  medium: String
  large: String
  width: Int!
  height: Int!
  size: Int!
  mimeType: String!
  alt: String
  caption: String
  uploadedBy: User!
  uploadedAt: DateTime!
}

type SEOMetadata {
  metaTitle: String
  metaDescription: String
  keywords: [String!]
  ogImage: String
  canonicalUrl: String
  noIndex: Boolean!
}

scalar DateTime
scalar JSON

Connection Types for Pagination

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type PostEdge {
  node: Post!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type CommentConnection {
  edges: [CommentEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type CommentEdge {
  node: Comment!
  cursor: String!
}

type SearchResult {
  posts: [Post!]!
  users: [User!]!
  tags: [Tag!]!
  totalResults: Int!
  took: Int!
}

type Analytics {
  totalPosts: Int!
  totalViews: Int!
  totalComments: Int!
  totalUsers: Int!
  popularPosts: [Post!]!
  recentActivity: [Activity!]!
  viewsByDate: [DateMetric!]!
}

type Activity {
  id: ID!
  type: ActivityType!
  user: User!
  post: Post
  comment: Comment
  description: String!
  createdAt: DateTime!
}

enum ActivityType {
  POST_CREATED
  POST_UPDATED
  POST_PUBLISHED
  COMMENT_ADDED
  USER_REGISTERED
  POST_LIKED
}

type DateMetric {
  date: String!
  count: Int!
}

πŸ” Queries & Mutations

Comprehensive API operations for content management and user interactions.

Query Operations

type Query {
  # Post queries
  post(id: ID, slug: String): Post
  posts(
    filter: PostFilter
    sort: PostSort
    first: Int
    after: String
    last: Int
    before: String
  ): PostConnection!
  
  # Search
  search(
    query: String!
    type: SearchType
    limit: Int
  ): SearchResult!
  
  # Category queries
  category(id: ID, slug: String): Category
  categories(parentId: ID): [Category!]!
  
  # Tag queries
  tag(id: ID, slug: String): Tag
  tags(limit: Int): [Tag!]!
  popularTags(limit: Int): [Tag!]!
  
  # Comment queries
  comment(id: ID!): Comment
  comments(
    postId: ID!
    status: CommentStatus
    first: Int
    after: String
  ): CommentConnection!
  
  # User queries
  user(id: ID, username: String): User
  users(role: UserRole, limit: Int, offset: Int): [User!]!
  me: User
  
  # Analytics
  analytics(startDate: DateTime, endDate: DateTime): Analytics!
  
  # Media
  image(id: ID!): Image
  images(limit: Int, offset: Int): [Image!]!
}

input PostFilter {
  status: PostStatus
  categoryId: ID
  tagIds: [ID!]
  authorId: ID
  search: String
  dateRange: DateRangeInput
}

enum PostSort {
  CREATED_DESC
  CREATED_ASC
  PUBLISHED_DESC
  PUBLISHED_ASC
  VIEWS_DESC
  LIKES_DESC
  TITLE_ASC
  TITLE_DESC
}

enum SearchType {
  ALL
  POSTS
  USERS
  TAGS
}

input DateRangeInput {
  start: DateTime
  end: DateTime
}

Mutation Operations

type Mutation {
  # Authentication
  register(input: RegisterInput!): AuthPayload!
  login(email: String!, password: String!): AuthPayload!
  logout: Boolean!
  refreshToken: AuthPayload!
  
  # Post mutations
  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
  publishPost(id: ID!): Post!
  schedulePost(id: ID!, publishAt: DateTime!): Post!
  likePost(id: ID!): Post!
  unlikePost(id: ID!): Post!
  incrementPostViews(id: ID!): Post!
  
  # Category mutations
  createCategory(input: CreateCategoryInput!): Category!
  updateCategory(id: ID!, input: UpdateCategoryInput!): Category!
  deleteCategory(id: ID!): Boolean!
  
  # Tag mutations
  createTag(input: CreateTagInput!): Tag!
  updateTag(id: ID!, input: UpdateTagInput!): Tag!
  deleteTag(id: ID!): Boolean!
  
  # Comment mutations
  createComment(input: CreateCommentInput!): Comment!
  updateComment(id: ID!, input: UpdateCommentInput!): Comment!
  deleteComment(id: ID!): Boolean!
  approveComment(id: ID!): Comment!
  rejectComment(id: ID!): Comment!
  markAsSpam(id: ID!): Comment!
  likeComment(id: ID!): Comment!
  
  # User mutations
  updateProfile(input: UpdateProfileInput!): User!
  changePassword(oldPassword: String!, newPassword: String!): Boolean!
  followUser(userId: ID!): User!
  unfollowUser(userId: ID!): User!
  updateUserRole(userId: ID!, role: UserRole!): User!
  
  # Media mutations
  uploadImage(file: Upload!, alt: String, caption: String): Image!
  updateImage(id: ID!, input: UpdateImageInput!): Image!
  deleteImage(id: ID!): Boolean!
}

input RegisterInput {
  email: String!
  password: String!
  username: String!
  firstName: String
  lastName: String
}

input CreatePostInput {
  title: String!
  content: String!
  excerpt: String
  featuredImageId: ID
  categoryId: ID
  tagIds: [ID!]
  status: PostStatus
  seo: SEOInput
}

input UpdatePostInput {
  title: String
  content: String
  excerpt: String
  featuredImageId: ID
  categoryId: ID
  tagIds: [ID!]
  status: PostStatus
  seo: SEOInput
}

input SEOInput {
  metaTitle: String
  metaDescription: String
  keywords: [String!]
  ogImage: String
  canonicalUrl: String
  noIndex: Boolean
}

input CreateCategoryInput {
  name: String!
  slug: String
  description: String
  parentId: ID
  imageId: ID
}

input UpdateCategoryInput {
  name: String
  slug: String
  description: String
  parentId: ID
  imageId: ID
}

input CreateTagInput {
  name: String!
  slug: String
}

input UpdateTagInput {
  name: String
  slug: String
}

input CreateCommentInput {
  postId: ID!
  content: String!
  parentId: ID
}

input UpdateCommentInput {
  content: String
}

input UpdateProfileInput {
  username: String
  firstName: String
  lastName: String
  bio: String
  avatarId: ID
  website: String
  socialLinks: SocialLinksInput
}

input SocialLinksInput {
  twitter: String
  facebook: String
  instagram: String
  linkedin: String
  github: String
}

input UpdateImageInput {
  alt: String
  caption: String
}

type AuthPayload {
  token: String!
  refreshToken: String!
  user: User!
  expiresIn: Int!
}

scalar Upload

πŸ’» Implementation Details

Key resolver implementations with business logic and database operations.

Post Resolvers

// resolvers/post.js
const { UserInputError, ForbiddenError } = require('apollo-server');
const { generateSlug, calculateReadingTime } = require('../utils');

const postResolvers = {
  Query: {
    post: async (_, { id, slug }, { dataSources, user }) => {
      const post = id
        ? await dataSources.postAPI.getPostById(id)
        : await dataSources.postAPI.getPostBySlug(slug);

      if (!post) {
        throw new UserInputError('Post not found');
      }

      // Only show published posts to non-authenticated users
      if (post.status !== 'PUBLISHED' && (!user || user.id !== post.authorId)) {
        throw new ForbiddenError('Access denied');
      }

      return post;
    },

    posts: async (_, { filter, sort, first, after, last, before }, { dataSources }) => {
      return await dataSources.postAPI.getPosts({
        filter,
        sort,
        first,
        after,
        last,
        before
      });
    },

    search: async (_, { query, type, limit }, { dataSources }) => {
      return await dataSources.searchAPI.search(query, type, limit);
    }
  },

  Mutation: {
    createPost: async (_, { input }, { user, dataSources }) => {
      if (!user) {
        throw new ForbiddenError('Authentication required');
      }

      if (!['ADMIN', 'EDITOR', 'AUTHOR'].includes(user.role)) {
        throw new ForbiddenError('Insufficient permissions');
      }

      const slug = generateSlug(input.title);
      const readingTime = calculateReadingTime(input.content);

      const post = await dataSources.postAPI.createPost({
        ...input,
        slug,
        readingTime,
        authorId: user.id,
        status: input.status || 'DRAFT'
      });

      // Create activity log
      await dataSources.activityAPI.createActivity({
        type: 'POST_CREATED',
        userId: user.id,
        postId: post.id,
        description: `created post "${post.title}"`
      });

      return post;
    },

    updatePost: async (_, { id, input }, { user, dataSources }) => {
      if (!user) {
        throw new ForbiddenError('Authentication required');
      }

      const post = await dataSources.postAPI.getPostById(id);
      
      if (!post) {
        throw new UserInputError('Post not found');
      }

      // Check permissions
      const canEdit = user.role === 'ADMIN' || 
                     user.role === 'EDITOR' || 
                     post.authorId === user.id;

      if (!canEdit) {
        throw new ForbiddenError('No permission to edit this post');
      }

      // Update slug if title changed
      if (input.title && input.title !== post.title) {
        input.slug = generateSlug(input.title);
      }

      // Recalculate reading time if content changed
      if (input.content) {
        input.readingTime = calculateReadingTime(input.content);
      }

      const updatedPost = await dataSources.postAPI.updatePost(id, input);

      // Create activity log
      await dataSources.activityAPI.createActivity({
        type: 'POST_UPDATED',
        userId: user.id,
        postId: id,
        description: `updated post "${updatedPost.title}"`
      });

      return updatedPost;
    },

    publishPost: async (_, { id }, { user, dataSources }) => {
      if (!user || !['ADMIN', 'EDITOR', 'AUTHOR'].includes(user.role)) {
        throw new ForbiddenError('Insufficient permissions');
      }

      const post = await dataSources.postAPI.getPostById(id);

      if (!post) {
        throw new UserInputError('Post not found');
      }

      if (user.role === 'AUTHOR' && post.authorId !== user.id) {
        throw new ForbiddenError('Cannot publish other authors\' posts');
      }

      const publishedPost = await dataSources.postAPI.updatePost(id, {
        status: 'PUBLISHED',
        publishedAt: new Date().toISOString()
      });

      // Create activity log
      await dataSources.activityAPI.createActivity({
        type: 'POST_PUBLISHED',
        userId: user.id,
        postId: id,
        description: `published post "${publishedPost.title}"`
      });

      // Clear cache
      await dataSources.cacheAPI.invalidate(`post:${id}`);

      return publishedPost;
    },

    likePost: async (_, { id }, { user, dataSources }) => {
      if (!user) {
        throw new ForbiddenError('Authentication required');
      }

      const like = await dataSources.likeAPI.likePost(id, user.id);
      const post = await dataSources.postAPI.getPostById(id);

      return post;
    },

    incrementPostViews: async (_, { id }, { dataSources }) => {
      await dataSources.postAPI.incrementViews(id);
      return await dataSources.postAPI.getPostById(id);
    }
  },

  Post: {
    author: (post, _, { dataSources }) => {
      return dataSources.userAPI.getUserById(post.authorId);
    },

    category: (post, _, { dataSources }) => {
      return post.categoryId
        ? dataSources.categoryAPI.getCategoryById(post.categoryId)
        : null;
    },

    tags: (post, _, { dataSources }) => {
      return dataSources.tagAPI.getTagsByPostId(post.id);
    },

    comments: async (post, _, { dataSources }) => {
      return await dataSources.commentAPI.getCommentsByPostId(post.id);
    },

    commentCount: async (post, _, { dataSources }) => {
      return await dataSources.commentAPI.getCommentCount(post.id);
    },

    featuredImage: (post, _, { dataSources }) => {
      return post.featuredImageId
        ? dataSources.imageAPI.getImageById(post.featuredImageId)
        : null;
    }
  }
};

module.exports = postResolvers;

Comment System with Moderation

// resolvers/comment.js
const { UserInputError, ForbiddenError } = require('apollo-server');
const { sendNotificationEmail } = require('../services/email');

const commentResolvers = {
  Query: {
    comments: async (_, { postId, status, first, after }, { dataSources, user }) => {
      // Show only approved comments to non-admins
      const commentStatus = (user && ['ADMIN', 'EDITOR'].includes(user.role))
        ? status
        : 'APPROVED';

      return await dataSources.commentAPI.getComments({
        postId,
        status: commentStatus,
        first,
        after
      });
    }
  },

  Mutation: {
    createComment: async (_, { input }, { user, dataSources }) => {
      if (!user) {
        throw new ForbiddenError('Authentication required to comment');
      }

      const { postId, content, parentId } = input;

      // Verify post exists
      const post = await dataSources.postAPI.getPostById(postId);
      if (!post) {
        throw new UserInputError('Post not found');
      }

      // Check if replying to existing comment
      if (parentId) {
        const parentComment = await dataSources.commentAPI.getCommentById(parentId);
        if (!parentComment || parentComment.postId !== postId) {
          throw new UserInputError('Invalid parent comment');
        }
      }

      // Create comment
      const comment = await dataSources.commentAPI.createComment({
        postId,
        authorId: user.id,
        content,
        parentId,
        status: 'APPROVED' // Auto-approve for now, can add moderation
      });

      // Send notification to post author
      if (post.authorId !== user.id) {
        await sendNotificationEmail({
          to: post.author.email,
          subject: 'New comment on your post',
          template: 'new-comment',
          data: {
            postTitle: post.title,
            commenterName: user.fullName,
            commentContent: content,
            postUrl: `/post/${post.slug}`
          }
        });
      }

      // If reply, notify parent comment author
      if (parentId) {
        const parentComment = await dataSources.commentAPI.getCommentById(parentId);
        if (parentComment.authorId !== user.id) {
          const parentAuthor = await dataSources.userAPI.getUserById(parentComment.authorId);
          await sendNotificationEmail({
            to: parentAuthor.email,
            subject: 'Someone replied to your comment',
            template: 'comment-reply',
            data: {
              replierName: user.fullName,
              replyContent: content,
              postTitle: post.title,
              postUrl: `/post/${post.slug}`
            }
          });
        }
      }

      // Create activity
      await dataSources.activityAPI.createActivity({
        type: 'COMMENT_ADDED',
        userId: user.id,
        postId,
        commentId: comment.id,
        description: `commented on "${post.title}"`
      });

      return comment;
    },

    approveComment: async (_, { id }, { user, dataSources }) => {
      if (!user || !['ADMIN', 'EDITOR'].includes(user.role)) {
        throw new ForbiddenError('Only admins and editors can moderate comments');
      }

      return await dataSources.commentAPI.updateCommentStatus(id, 'APPROVED');
    },

    markAsSpam: async (_, { id }, { user, dataSources }) => {
      if (!user || !['ADMIN', 'EDITOR'].includes(user.role)) {
        throw new ForbiddenError('Insufficient permissions');
      }

      return await dataSources.commentAPI.updateCommentStatus(id, 'SPAM');
    },

    deleteComment: async (_, { id }, { user, dataSources }) => {
      if (!user) {
        throw new ForbiddenError('Authentication required');
      }

      const comment = await dataSources.commentAPI.getCommentById(id);
      
      if (!comment) {
        throw new UserInputError('Comment not found');
      }

      // Check permissions
      const canDelete = user.role === 'ADMIN' || 
                       user.role === 'EDITOR' || 
                       comment.authorId === user.id;

      if (!canDelete) {
        throw new ForbiddenError('No permission to delete this comment');
      }

      await dataSources.commentAPI.deleteComment(id);
      return true;
    },

    likeComment: async (_, { id }, { user, dataSources }) => {
      if (!user) {
        throw new ForbiddenError('Authentication required');
      }

      await dataSources.likeAPI.likeComment(id, user.id);
      return await dataSources.commentAPI.getCommentById(id);
    }
  },

  Comment: {
    author: (comment, _, { dataSources }) => {
      return dataSources.userAPI.getUserById(comment.authorId);
    },

    post: (comment, _, { dataSources }) => {
      return dataSources.postAPI.getPostById(comment.postId);
    },

    parent: (comment, _, { dataSources }) => {
      return comment.parentId
        ? dataSources.commentAPI.getCommentById(comment.parentId)
        : null;
    },

    replies: async (comment, _, { dataSources }) => {
      return await dataSources.commentAPI.getReplies(comment.id);
    }
  }
};

module.exports = commentResolvers;

πŸ” Authentication & Authorization

Secure user authentication with JWT tokens and role-based access control.

Authentication Implementation

// services/auth.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const { AuthenticationError } = require('apollo-server');

const JWT_SECRET = process.env.JWT_SECRET;
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
const JWT_EXPIRES_IN = '1h';
const REFRESH_TOKEN_EXPIRES_IN = '7d';

class AuthService {
  // Generate JWT token
  generateToken(user) {
    return jwt.sign(
      {
        id: user.id,
        email: user.email,
        role: user.role
      },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );
  }

  // Generate refresh token
  generateRefreshToken(user) {
    return jwt.sign(
      { id: user.id },
      JWT_REFRESH_SECRET,
      { expiresIn: REFRESH_TOKEN_EXPIRES_IN }
    );
  }

  // Verify token
  verifyToken(token) {
    try {
      return jwt.verify(token, JWT_SECRET);
    } catch (error) {
      throw new AuthenticationError('Invalid or expired token');
    }
  }

  // Verify refresh token
  verifyRefreshToken(token) {
    try {
      return jwt.verify(token, JWT_REFRESH_SECRET);
    } catch (error) {
      throw new AuthenticationError('Invalid or expired refresh token');
    }
  }

  // Hash password
  async hashPassword(password) {
    return await bcrypt.hash(password, 12);
  }

  // Compare password
  async comparePassword(password, hashedPassword) {
    return await bcrypt.compare(password, hashedPassword);
  }

  // Register user
  async register(userAPI, input) {
    const { email, password, username, firstName, lastName } = input;

    // Check if user exists
    const existingUser = await userAPI.getUserByEmail(email);
    if (existingUser) {
      throw new UserInputError('Email already in use');
    }

    const existingUsername = await userAPI.getUserByUsername(username);
    if (existingUsername) {
      throw new UserInputError('Username already taken');
    }

    // Hash password
    const hashedPassword = await this.hashPassword(password);

    // Create user
    const user = await userAPI.createUser({
      email,
      username,
      password: hashedPassword,
      firstName,
      lastName,
      role: 'READER' // Default role
    });

    // Generate tokens
    const token = this.generateToken(user);
    const refreshToken = this.generateRefreshToken(user);

    // Store refresh token
    await userAPI.saveRefreshToken(user.id, refreshToken);

    return {
      token,
      refreshToken,
      user,
      expiresIn: 3600 // 1 hour in seconds
    };
  }

  // Login user
  async login(userAPI, email, password) {
    // Find user
    const user = await userAPI.getUserByEmail(email);
    if (!user) {
      throw new AuthenticationError('Invalid credentials');
    }

    // Verify password
    const isValid = await this.comparePassword(password, user.password);
    if (!isValid) {
      throw new AuthenticationError('Invalid credentials');
    }

    // Generate tokens
    const token = this.generateToken(user);
    const refreshToken = this.generateRefreshToken(user);

    // Store refresh token
    await userAPI.saveRefreshToken(user.id, refreshToken);

    return {
      token,
      refreshToken,
      user,
      expiresIn: 3600
    };
  }

  // Refresh token
  async refreshToken(userAPI, refreshToken) {
    // Verify refresh token
    const payload = this.verifyRefreshToken(refreshToken);

    // Get user
    const user = await userAPI.getUserById(payload.id);
    if (!user) {
      throw new AuthenticationError('User not found');
    }

    // Verify stored refresh token
    const isValid = await userAPI.verifyRefreshToken(user.id, refreshToken);
    if (!isValid) {
      throw new AuthenticationError('Invalid refresh token');
    }

    // Generate new tokens
    const newToken = this.generateToken(user);
    const newRefreshToken = this.generateRefreshToken(user);

    // Store new refresh token
    await userAPI.saveRefreshToken(user.id, newRefreshToken);

    return {
      token: newToken,
      refreshToken: newRefreshToken,
      user,
      expiresIn: 3600
    };
  }
}

module.exports = new AuthService();

Authorization Directives

// directives/auth.js
const { SchemaDirectiveVisitor } = require('apollo-server');
const { defaultFieldResolver } = require('graphql');
const { AuthenticationError, ForbiddenError } = require('apollo-server');

// @auth directive - requires authentication
class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    
    field.resolve = async function (...args) {
      const context = args[2];
      
      if (!context.user) {
        throw new AuthenticationError('Authentication required');
      }
      
      return resolve.apply(this, args);
    };
  }
}

// @hasRole directive - requires specific role
class HasRoleDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    const { roles } = this.args;
    
    field.resolve = async function (...args) {
      const context = args[2];
      
      if (!context.user) {
        throw new AuthenticationError('Authentication required');
      }
      
      if (!roles.includes(context.user.role)) {
        throw new ForbiddenError(
          `Requires one of these roles: ${roles.join(', ')}`
        );
      }
      
      return resolve.apply(this, args);
    };
  }
}

// Usage in schema
const typeDefs = gql`
  directive @auth on FIELD_DEFINITION
  directive @hasRole(roles: [UserRole!]!) on FIELD_DEFINITION

  type Query {
    me: User @auth
    analytics: Analytics @hasRole(roles: [ADMIN, EDITOR])
  }

  type Mutation {
    createPost(input: CreatePostInput!): Post! @hasRole(roles: [ADMIN, EDITOR, AUTHOR])
    deleteUser(id: ID!): Boolean! @hasRole(roles: [ADMIN])
    updateProfile(input: UpdateProfileInput!): User! @auth
  }
`;

module.exports = { AuthDirective, HasRoleDirective };

🌐 SEO & Performance

Optimization strategies for search engines and fast content delivery.

πŸ”

SEO Metadata

Automatic generation of meta tags, Open Graph, Twitter Cards, and structured data (JSON-LD) for better search visibility.

πŸ—ΊοΈ

XML Sitemap

Dynamic sitemap generation with priority and change frequency for efficient crawling by search engines.

⚑

Redis Caching

Cache frequently accessed posts and queries with smart invalidation strategies for lightning-fast response times.

SEO Utilities

// utils/seo.js

// Generate slug from title
function generateSlug(title) {
  return title
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, '')
    .replace(/[\s_-]+/g, '-')
    .replace(/^-+|-+$/g, '');
}

// Calculate reading time
function calculateReadingTime(content) {
  const wordsPerMinute = 200;
  const words = content.trim().split(/\s+/).length;
  return Math.ceil(words / wordsPerMinute);
}

// Generate meta description from content
function generateMetaDescription(content, maxLength = 160) {
  const plainText = content
    .replace(/<[^>]*>/g, '') // Remove HTML tags
    .replace(/\s+/g, ' ') // Normalize whitespace
    .trim();
  
  if (plainText.length <= maxLength) {
    return plainText;
  }
  
  return plainText.substring(0, maxLength - 3) + '...';
}

// Generate structured data (JSON-LD)
function generateStructuredData(post) {
  return {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: post.title,
    description: post.excerpt || generateMetaDescription(post.content),
    image: post.featuredImage?.url,
    author: {
      '@type': 'Person',
      name: post.author.fullName,
      url: `/author/${post.author.username}`
    },
    publisher: {
      '@type': 'Organization',
      name: 'Your Blog Name',
      logo: {
        '@type': 'ImageObject',
        url: 'https://yourblog.com/logo.png'
      }
    },
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': `https://yourblog.com/post/${post.slug}`
    }
  };
}

// Generate XML sitemap
async function generateSitemap(postAPI) {
  const posts = await postAPI.getAllPublishedPosts();
  
  let xml = '\n';
  xml += '\n';
  
  // Homepage
  xml += '  \n';
  xml += '    https://yourblog.com/\n';
  xml += '    daily\n';
  xml += '    1.0\n';
  xml += '  \n';
  
  // Posts
  posts.forEach(post => {
    xml += '  \n';
    xml += `    https://yourblog.com/post/${post.slug}\n`;
    xml += `    ${post.updatedAt}\n`;
    xml += '    weekly\n';
    xml += '    0.8\n';
    xml += '  \n';
  });
  
  xml += '';
  
  return xml;
}

// Generate RSS feed
async function generateRSSFeed(postAPI) {
  const posts = await postAPI.getRecentPosts(20);
  
  let xml = '\n';
  xml += '\n';
  xml += '  \n';
  xml += '    Your Blog Name\n';
  xml += '    https://yourblog.com\n';
  xml += '    Blog description\n';
  
  posts.forEach(post => {
    xml += '    \n';
    xml += `      ${post.title}\n`;
    xml += `      https://yourblog.com/post/${post.slug}\n`;
    xml += `      ${post.excerpt || ''}\n`;
    xml += `      ${new Date(post.publishedAt).toUTCString()}\n`;
    xml += '    \n';
  });
  
  xml += '  \n';
  xml += '';
  
  return xml;
}

module.exports = {
  generateSlug,
  calculateReadingTime,
  generateMetaDescription,
  generateStructuredData,
  generateSitemap,
  generateRSSFeed
};

Caching Strategy

// services/cache.js
const Redis = require('ioredis');

class CacheService {
  constructor() {
    this.redis = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: process.env.REDIS_PORT || 6379,
      password: process.env.REDIS_PASSWORD
    });
  }

  // Cache post
  async cachePost(post) {
    const key = `post:${post.id}`;
    await this.redis.setex(key, 3600, JSON.stringify(post)); // 1 hour
  }

  // Get cached post
  async getCachedPost(postId) {
    const key = `post:${postId}`;
    const cached = await this.redis.get(key);
    return cached ? JSON.parse(cached) : null;
  }

  // Cache post list
  async cachePostList(filter, posts) {
    const key = `posts:${JSON.stringify(filter)}`;
    await this.redis.setex(key, 600, JSON.stringify(posts)); // 10 minutes
  }

  // Invalidate post cache
  async invalidatePost(postId) {
    await this.redis.del(`post:${postId}`);
    // Also invalidate related lists
    const keys = await this.redis.keys('posts:*');
    if (keys.length > 0) {
      await this.redis.del(...keys);
    }
  }

  // Cache popular posts
  async cachePopularPosts(posts) {
    await this.redis.setex('popular:posts', 1800, JSON.stringify(posts)); // 30 minutes
  }

  // Get cached popular posts
  async getCachedPopularPosts() {
    const cached = await this.redis.get('popular:posts');
    return cached ? JSON.parse(cached) : null;
  }
}

module.exports = new CacheService();

πŸš€ Deployment

Production deployment with Docker, environment configuration, and best practices.

Docker Configuration

# docker-compose.yml
version: '3.8'

services:
  # GraphQL API
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "4000:4000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:password@postgres:5432/blog
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}
      - AWS_S3_BUCKET=${AWS_S3_BUCKET}
      - SMTP_HOST=${SMTP_HOST}
      - SMTP_USER=${SMTP_USER}
      - SMTP_PASS=${SMTP_PASS}
    depends_on:
      - postgres
      - redis
    restart: unless-stopped

  # PostgreSQL
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=blog
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    restart: unless-stopped

  # Redis
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    restart: unless-stopped

  # Nginx (Optional - for serving frontend)
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - api
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

Environment Variables

# .env.production
NODE_ENV=production
PORT=4000

# Database
DATABASE_URL=postgresql://user:password@host:5432/blog

# Redis
REDIS_URL=redis://host:6379
REDIS_HOST=host
REDIS_PORT=6379

# JWT
JWT_SECRET=your-secret-key-change-in-production
JWT_REFRESH_SECRET=your-refresh-secret
JWT_EXPIRES_IN=1h

# AWS S3
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_S3_BUCKET=blog-media
AWS_REGION=us-east-1

# Email (SMTP)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=noreply@yourblog.com

# Site Configuration
SITE_URL=https://yourblog.com
SITE_NAME=Your Blog Name

# API
CORS_ORIGIN=https://yourblog.com,https://admin.yourblog.com

# Rate Limiting
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=900000

# Logging
LOG_LEVEL=info
SENTRY_DSN=your-sentry-dsn

Key Learnings

πŸ“ Content Modeling

Flexible content structure with categories, tags, and custom fields makes the system adaptable to different blogging needs.

πŸ”’ Security First

Role-based access control and input validation prevent unauthorized access and malicious content injection.

⚑ Performance Matters

Caching with Redis and DataLoader significantly reduce database queries and improve response times for popular content.

πŸ” SEO is Critical

Proper meta tags, structured data, and sitemap generation are essential for content discoverability in search engines.

πŸ’¬ User Engagement

Comment system with moderation and email notifications keeps readers engaged while preventing spam.

πŸ“Š Analytics Integration

Tracking views, engagement, and popular content helps understand what resonates with your audience.

πŸŽ“ Conclusion

This Blog API demonstrates a production-ready content management system built with GraphQL. The architecture supports multiple clients (web, mobile, admin) through a unified API with comprehensive features for content creation, user management, and SEO optimization.

Key achievements include role-based permissions, rich text editing, comment moderation, media management, and performance optimization through caching. The API-first design enables building any type of frontend while maintaining consistent business logic.

GraphQL Apollo Server Node.js PostgreSQL Redis JWT Docker AWS S3 DataLoader Prisma