Skip to content
Go back

GraphQL TypeScript: API Development and Code Generation

GraphQL TypeScript: API Development and Code Generation

GraphQL with TypeScript provides a powerful combination for building type-safe, efficient APIs. This comprehensive guide covers schema design, resolver implementation, code generation, performance optimization, and production deployment.

Why Choose GraphQL with TypeScript?

Step 1: Project Setup and Configuration

Set up a comprehensive GraphQL TypeScript development environment:

# Create project directory
mkdir graphql-typescript-api
cd graphql-typescript-api

# Initialize package.json
npm init -y

# Install GraphQL dependencies
npm install graphql apollo-server-express
npm install @apollo/server
npm install graphql-scalars graphql-tools
npm install dataloader

# Install code generation tools
npm install -D @graphql-codegen/cli
npm install -D @graphql-codegen/typescript
npm install -D @graphql-codegen/typescript-resolvers
npm install -D @graphql-codegen/typescript-operations

# Install TypeScript and dependencies
npm install -D typescript @types/node
npm install -D nodemon ts-node concurrently

# Install additional dependencies
npm install express cors helmet
npm install @prisma/client prisma
npm install jsonwebtoken bcryptjs
npm install zod

# Install development tools
npm install -D jest @types/jest ts-jest
npm install -D eslint @typescript-eslint/eslint-plugin
npm install -D prettier

Configure TypeScript for GraphQL development:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "CommonJS",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "resolveJsonModule": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@/types/*": ["src/types/*"],
      "@/resolvers/*": ["src/resolvers/*"],
      "@/schema/*": ["src/schema/*"],
      "@/utils/*": ["src/utils/*"],
      "@/services/*": ["src/services/*"],
      "@/generated/*": ["src/generated/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}tsconfig.json

Step 2: GraphQL Schema Definition

Create a comprehensive GraphQL schema using schema-first approach:

# Scalars
scalar DateTime
scalar JSON
scalar Upload
scalar EmailAddress
scalar URL

# User Types
type User {
  id: ID!
  email: EmailAddress!
  username: String!
  firstName: String!
  lastName: String!
  fullName: String!
  avatar: URL
  bio: String
  website: URL
  location: String
  role: UserRole!
  status: UserStatus!
  isOnline: Boolean!
  lastSeenAt: DateTime
  emailVerified: Boolean!
  createdAt: DateTime!
  updatedAt: DateTime!

  # Relationships
  posts(first: Int, after: String, orderBy: PostOrderBy): PostConnection!
  followers(first: Int, after: String): UserConnection!
  following(first: Int, after: String): UserConnection!
  addresses: [Address!]!

  # Counts
  postsCount: Int!
  followersCount: Int!
  followingCount: Int!
}

type Address {
  id: ID!
  type: AddressType!
  firstName: String!
  lastName: String!
  company: String
  street1: String!
  street2: String
  city: String!
  state: String!
  zipCode: String!
  country: String!
  isDefault: Boolean!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# Post Types
type Post {
  id: ID!
  title: String!
  content: String!
  excerpt: String
  slug: String!
  status: PostStatus!
  featuredImage: URL
  seoTitle: String
  seoDescription: String
  viewCount: Int!
  likeCount: Int!
  commentCount: Int!
  publishedAt: DateTime
  createdAt: DateTime!
  updatedAt: DateTime!

  # Relationships
  author: User!
  comments(first: Int, after: String): CommentConnection!
  likes(first: Int, after: String): UserConnection!
  tags: [Tag!]!

  # User-specific fields
  isLiked: Boolean!
  canEdit: Boolean!
}

type Comment {
  id: ID!
  content: String!
  status: CommentStatus!
  likeCount: Int!
  createdAt: DateTime!
  updatedAt: DateTime!

  # Relationships
  author: User!
  post: Post!
  parent: Comment
  replies(first: Int, after: String): CommentConnection!

  # User-specific fields
  isLiked: Boolean!
  canEdit: Boolean!
}

type Tag {
  id: ID!
  name: String!
  slug: String!
  color: String
  postsCount: Int!

  # Relationships
  posts(first: Int, after: String): PostConnection!
}

# Product Types
type Product {
  id: ID!
  name: String!
  description: String!
  slug: String!
  price: Float!
  originalPrice: Float
  currency: String!
  sku: String
  stock: Int!
  images: [URL!]!
  status: ProductStatus!
  isFeatured: Boolean!
  createdAt: DateTime!
  updatedAt: DateTime!

  # Relationships
  category: Category!
  brand: Brand
  reviews(first: Int, after: String): ReviewConnection!
  variants: [ProductVariant!]!

  # Computed fields
  averageRating: Float
  reviewCount: Int!
  isInStock: Boolean!
}

type ProductVariant {
  id: ID!
  name: String!
  sku: String
  price: Float
  stock: Int!
  attributes: JSON!
  image: URL
  isActive: Boolean!
}

type Category {
  id: ID!
  name: String!
  slug: String!
  description: String
  image: URL
  parent: Category
  children: [Category!]!
  productsCount: Int!
  isActive: Boolean!

  # Relationships
  products(
    first: Int
    after: String
    filters: ProductFilters
  ): ProductConnection!
}

type Brand {
  id: ID!
  name: String!
  slug: String!
  description: String
  logo: URL
  website: URL
  isActive: Boolean!

  # Relationships
  products(first: Int, after: String): ProductConnection!
}

type Review {
  id: ID!
  rating: Int!
  title: String
  content: String!
  images: [URL!]!
  isVerified: Boolean!
  helpfulCount: Int!
  status: ReviewStatus!
  createdAt: DateTime!
  updatedAt: DateTime!

  # Relationships
  author: User!
  product: Product!

  # User-specific fields
  isHelpful: Boolean!
}

# Order Types
type Order {
  id: ID!
  orderNumber: String!
  status: OrderStatus!
  subtotal: Float!
  taxAmount: Float!
  shippingCost: Float!
  totalAmount: Float!
  currency: String!
  paymentStatus: PaymentStatus!
  shippingMethod: String
  trackingNumber: String
  estimatedDelivery: DateTime
  createdAt: DateTime!
  updatedAt: DateTime!

  # Relationships
  customer: User!
  items: [OrderItem!]!
  shippingAddress: Address
  billingAddress: Address
}

type OrderItem {
  id: ID!
  name: String!
  price: Float!
  quantity: Int!
  totalPrice: Float!
  productSnapshot: JSON!

  # Relationships
  product: Product!
  variant: ProductVariant
}

# Notification Types
type Notification {
  id: ID!
  title: String!
  message: String!
  type: NotificationType!
  isRead: Boolean!
  actionUrl: String
  createdAt: DateTime!

  # Relationships
  user: User!
}

# Connection Types (for pagination)
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!
}

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

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

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

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

type ProductConnection {
  edges: [ProductEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type ProductEdge {
  node: Product!
  cursor: String!
}

type ReviewConnection {
  edges: [ReviewEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type ReviewEdge {
  node: Review!
  cursor: String!
}

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

# Enums
enum UserRole {
  ADMIN
  MODERATOR
  USER
  GUEST
}

enum UserStatus {
  ACTIVE
  INACTIVE
  SUSPENDED
  BANNED
}

enum AddressType {
  HOME
  WORK
  BILLING
  SHIPPING
  OTHER
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

enum CommentStatus {
  PENDING
  APPROVED
  REJECTED
  SPAM
}

enum ProductStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
  OUT_OF_STOCK
}

enum OrderStatus {
  PENDING
  CONFIRMED
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
  REFUNDED
}

enum PaymentStatus {
  PENDING
  COMPLETED
  FAILED
  CANCELLED
  REFUNDED
}

enum ReviewStatus {
  PENDING
  APPROVED
  REJECTED
}

enum NotificationType {
  ORDER_UPDATE
  MESSAGE
  FOLLOW
  LIKE
  COMMENT
  SYSTEM
  MARKETING
}

# Input Types
input CreateUserInput {
  email: EmailAddress!
  username: String!
  firstName: String!
  lastName: String!
  password: String!
  phone: String
}

input UpdateUserInput {
  username: String
  firstName: String
  lastName: String
  bio: String
  website: URL
  location: String
}

input CreatePostInput {
  title: String!
  content: String!
  excerpt: String
  featuredImage: URL
  seoTitle: String
  seoDescription: String
  tagIds: [ID!]
  status: PostStatus = DRAFT
}

input UpdatePostInput {
  title: String
  content: String
  excerpt: String
  featuredImage: URL
  seoTitle: String
  seoDescription: String
  tagIds: [ID!]
  status: PostStatus
}

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

input UpdateCommentInput {
  content: String!
}

input CreateProductInput {
  name: String!
  description: String!
  price: Float!
  categoryId: ID!
  brandId: ID
  sku: String
  stock: Int!
  images: [URL!]!
  status: ProductStatus = DRAFT
}

input UpdateProductInput {
  name: String
  description: String
  price: Float
  categoryId: ID
  brandId: ID
  sku: String
  stock: Int
  images: [URL!]
  status: ProductStatus
}

input CreateReviewInput {
  productId: ID!
  rating: Int!
  title: String
  content: String!
  images: [URL!]
}

input CreateAddressInput {
  type: AddressType!
  firstName: String!
  lastName: String!
  company: String
  street1: String!
  street2: String
  city: String!
  state: String!
  zipCode: String!
  country: String!
  isDefault: Boolean = false
}

input UpdateAddressInput {
  type: AddressType
  firstName: String
  lastName: String
  company: String
  street1: String
  street2: String
  city: String
  state: String
  zipCode: String
  country: String
  isDefault: Boolean
}

# Filter Types
input ProductFilters {
  categoryId: ID
  brandId: ID
  priceRange: PriceRangeInput
  inStock: Boolean
  isFeatured: Boolean
  status: ProductStatus
}

input PriceRangeInput {
  min: Float
  max: Float
}

# Order By Types
input PostOrderBy {
  field: PostOrderField!
  direction: OrderDirection!
}

enum PostOrderField {
  CREATED_AT
  UPDATED_AT
  PUBLISHED_AT
  TITLE
  VIEW_COUNT
  LIKE_COUNT
}

enum OrderDirection {
  ASC
  DESC
}

# Authentication Types
type AuthPayload {
  token: String!
  refreshToken: String!
  user: User!
}

input LoginInput {
  email: EmailAddress!
  password: String!
}

# Query Type
type Query {
  # Authentication
  me: User

  # Users
  user(id: ID, username: String): User
  users(
    first: Int
    after: String
    search: String
    role: UserRole
  ): UserConnection!

  # Posts
  post(id: ID, slug: String): Post
  posts(
    first: Int
    after: String
    authorId: ID
    status: PostStatus
    orderBy: PostOrderBy
  ): PostConnection!

  # Comments
  comment(id: ID!): Comment

  # Products
  product(id: ID, slug: String): Product
  products(
    first: Int
    after: String
    filters: ProductFilters
  ): ProductConnection!

  # Categories
  category(id: ID, slug: String): Category
  categories(parentId: ID): [Category!]!

  # Brands
  brand(id: ID, slug: String): Brand
  brands(first: Int, after: String): [Brand!]!

  # Tags
  tag(id: ID, slug: String): Tag
  tags(search: String): [Tag!]!

  # Orders
  order(id: ID!): Order
  myOrders(first: Int, after: String): [Order!]!

  # Notifications
  myNotifications(
    first: Int
    after: String
    unreadOnly: Boolean
  ): [Notification!]!
  unreadNotificationsCount: Int!

  # Search
  search(
    query: String!
    type: SearchType
    first: Int
    after: String
  ): SearchResult!
}

enum SearchType {
  ALL
  USERS
  POSTS
  PRODUCTS
}

union SearchResult = UserConnection | PostConnection | ProductConnection

# Mutation Type
type Mutation {
  # Authentication
  register(input: CreateUserInput!): AuthPayload!
  login(input: LoginInput!): AuthPayload!
  refreshToken(refreshToken: String!): AuthPayload!
  logout: Boolean!

  # User Management
  updateProfile(input: UpdateUserInput!): User!
  changePassword(currentPassword: String!, newPassword: String!): Boolean!
  deleteAccount: Boolean!

  # User Actions
  followUser(userId: ID!): Boolean!
  unfollowUser(userId: ID!): Boolean!

  # Address Management
  createAddress(input: CreateAddressInput!): Address!
  updateAddress(id: ID!, input: UpdateAddressInput!): Address!
  deleteAddress(id: ID!): Boolean!

  # Posts
  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
  publishPost(id: ID!): Post!
  likePost(postId: ID!): Boolean!
  unlikePost(postId: ID!): Boolean!

  # Comments
  createComment(input: CreateCommentInput!): Comment!
  updateComment(id: ID!, input: UpdateCommentInput!): Comment!
  deleteComment(id: ID!): Boolean!
  likeComment(commentId: ID!): Boolean!
  unlikeComment(commentId: ID!): Boolean!

  # Products
  createProduct(input: CreateProductInput!): Product!
  updateProduct(id: ID!, input: UpdateProductInput!): Product!
  deleteProduct(id: ID!): Boolean!

  # Reviews
  createReview(input: CreateReviewInput!): Review!
  updateReview(id: ID!, rating: Int, title: String, content: String): Review!
  deleteReview(id: ID!): Boolean!
  markReviewHelpful(reviewId: ID!): Boolean!

  # Notifications
  markNotificationRead(id: ID!): Notification!
  markAllNotificationsRead: Boolean!
  deleteNotification(id: ID!): Boolean!

  # File Upload
  uploadFile(file: Upload!): URL!
}

# Subscription Type
type Subscription {
  # Real-time notifications
  notificationAdded(userId: ID!): Notification!

  # Post updates
  postLiked(postId: ID!): Post!
  commentAdded(postId: ID!): Comment!

  # User status
  userOnlineStatus(userId: ID!): User!

  # Order updates
  orderStatusChanged(userId: ID!): Order!
}src/schema/schema.graphql

Step 3: Code Generation Configuration

Set up GraphQL Code Generator for type-safe development:

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  overwrite: true,
  schema: "./src/schema/schema.graphql",
  documents: "./src/**/*.{ts,tsx,graphql}",
  generates: {
    // Generate TypeScript types from schema
    "./src/generated/graphql.ts": {
      plugins: [
        "typescript",
        "typescript-resolvers",
        {
          add: {
            content: "/* eslint-disable */",
          },
        },
      ],
      config: {
        useIndexSignature: true,
        contextType: "../types/context#GraphQLContext",
        mappers: {
          User: "../types/prisma#UserWithRelations",
          Post: "../types/prisma#PostWithRelations",
          Product: "../types/prisma#ProductWithRelations",
          Order: "../types/prisma#OrderWithRelations",
          Comment: "../types/prisma#CommentWithRelations",
          Review: "../types/prisma#ReviewWithRelations",
          Address: "@prisma/client#Address",
          Category: "@prisma/client#Category",
          Brand: "@prisma/client#Brand",
          Tag: "@prisma/client#Tag",
          Notification: "@prisma/client#Notification",
        },
        scalars: {
          DateTime: "Date",
          JSON: "any",
          Upload: "Promise<FileUpload>",
          EmailAddress: "string",
          URL: "string",
        },
        maybeValue: "T | null | undefined",
        inputMaybeValue: "T | null | undefined",
        enumsAsTypes: true,
        constEnums: true,
        numericEnums: true,
        skipTypename: false,
        avoidOptionals: {
          field: true,
          inputValue: false,
          object: false,
        },
        directiveMap: {
          auth: "authDirective",
          rateLimit: "rateLimitDirective",
        },
      },
    },

    // Generate operations types for client-side
    "./src/generated/operations.ts": {
      documents: "./src/operations/**/*.graphql",
      plugins: [
        "typescript",
        "typescript-operations",
        {
          add: {
            content: "/* eslint-disable */",
          },
        },
      ],
      config: {
        preResolveTypes: true,
        skipTypename: false,
        enumsAsTypes: true,
        constEnums: true,
        maybeValue: "T | null | undefined",
      },
    },

    // Generate React Apollo hooks (if using React)
    "./src/generated/react-apollo.ts": {
      documents: "./src/operations/**/*.graphql",
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo",
        {
          add: {
            content: "/* eslint-disable */",
          },
        },
      ],
      config: {
        withHooks: true,
        withHOC: false,
        withComponent: false,
        apolloReactHooksImportFrom: "@apollo/client",
      },
    },

    // Generate fragment matcher
    "./src/generated/fragment-matcher.json": {
      plugins: ["fragment-matcher"],
    },

    // Generate introspection result
    "./src/generated/introspection.json": {
      plugins: ["introspection"],
    },
  },
  hooks: {
    afterAllFileWrite: ["prettier --write"],
  },
};

export default config;codegen.ts

Step 4: Resolver Implementation

Create comprehensive GraphQL resolvers with type safety:

import {
  AuthenticationError,
  ForbiddenError,
  UserInputError,
} from "apollo-server-express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { z } from "zod";

import {
  Resolvers,
  User,
  CreateUserInput,
  UpdateUserInput,
  LoginInput,
  AuthPayload,
  UserConnection,
  MutationResolvers,
  QueryResolvers,
  UserResolvers as UserFieldResolvers,
} from "@/generated/graphql";
import { GraphQLContext } from "@/types/context";
import { UserWithRelations } from "@/types/prisma";
import { validateInput } from "@/utils/validation";
import { createCursor, parseCursor } from "@/utils/pagination";

// Validation schemas
const createUserSchema = z.object({
  email: z.string().email(),
  username: z.string().min(3).max(50),
  firstName: z.string().min(1).max(100),
  lastName: z.string().min(1).max(100),
  password: z.string().min(8),
  phone: z.string().optional(),
});

const updateUserSchema = z.object({
  username: z.string().min(3).max(50).optional(),
  firstName: z.string().min(1).max(100).optional(),
  lastName: z.string().min(1).max(100).optional(),
  bio: z.string().max(500).optional(),
  website: z.string().url().optional(),
  location: z.string().max(100).optional(),
});

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(1),
});

// Helper functions
const hashPassword = async (password: string): Promise<string> => {
  return bcrypt.hash(password, 12);
};

const verifyPassword = async (
  password: string,
  hash: string
): Promise<boolean> => {
  return bcrypt.compare(password, hash);
};

const generateTokens = (user: UserWithRelations) => {
  const payload = {
    userId: user.id,
    email: user.email,
    role: user.role,
  };

  const token = jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "1d" });
  const refreshToken = jwt.sign(
    { userId: user.id, type: "refresh" },
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: "7d" }
  );

  return { token, refreshToken };
};

// Query resolvers
const Query: QueryResolvers<GraphQLContext> = {
  me: async (_, __, { user, dataSources }) => {
    if (!user) return null;

    return dataSources.userAPI.findById(user.id);
  },

  user: async (_, { id, username }, { dataSources }) => {
    if (id) {
      return dataSources.userAPI.findById(id);
    }

    if (username) {
      return dataSources.userAPI.findByUsername(username);
    }

    throw new UserInputError("Either id or username must be provided");
  },

  users: async (_, { first = 20, after, search, role }, { dataSources }) => {
    const limit = Math.min(first, 100); // Cap at 100
    const cursor = after ? parseCursor(after) : null;

    const filters: any = {};

    if (search) {
      filters.search = search;
    }

    if (role) {
      filters.role = role;
    }

    const result = await dataSources.userAPI.findMany({
      filters,
      pagination: {
        limit,
        cursor,
      },
    });

    const edges = result.data.map(user => ({
      node: user,
      cursor: createCursor(user.id),
    }));

    return {
      edges,
      pageInfo: {
        hasNextPage: result.hasNextPage,
        hasPreviousPage: !!after,
        startCursor: edges[0]?.cursor,
        endCursor: edges[edges.length - 1]?.cursor,
      },
      totalCount: result.totalCount,
    };
  },
};

// Mutation resolvers
const Mutation: MutationResolvers<GraphQLContext> = {
  register: async (_, { input }, { dataSources }) => {
    // Validate input
    const validatedInput = validateInput(createUserSchema, input);

    // Check if user already exists
    const existingUser = await dataSources.userAPI.findByEmail(
      validatedInput.email
    );
    if (existingUser) {
      throw new UserInputError("User with this email already exists");
    }

    const existingUsername = await dataSources.userAPI.findByUsername(
      validatedInput.username
    );
    if (existingUsername) {
      throw new UserInputError("Username is already taken");
    }

    // Hash password
    const passwordHash = await hashPassword(validatedInput.password);

    // Create user
    const user = await dataSources.userAPI.create({
      ...validatedInput,
      passwordHash,
    });

    // Generate tokens
    const { token, refreshToken } = generateTokens(user);

    return {
      token,
      refreshToken,
      user,
    };
  },

  login: async (_, { input }, { dataSources }) => {
    // Validate input
    const validatedInput = validateInput(loginSchema, input);

    // Find user
    const user = await dataSources.userAPI.findByEmail(validatedInput.email);
    if (!user) {
      throw new AuthenticationError("Invalid credentials");
    }

    // Verify password
    const isPasswordValid = await verifyPassword(
      validatedInput.password,
      user.passwordHash
    );
    if (!isPasswordValid) {
      throw new AuthenticationError("Invalid credentials");
    }

    // Check if user is active
    if (user.status !== "ACTIVE") {
      throw new ForbiddenError("Account is not active");
    }

    // Update last seen
    await dataSources.userAPI.updateLastSeen(user.id);

    // Generate tokens
    const { token, refreshToken } = generateTokens(user);

    return {
      token,
      refreshToken,
      user,
    };
  },

  refreshToken: async (_, { refreshToken }, { dataSources }) => {
    try {
      const decoded = jwt.verify(
        refreshToken,
        process.env.JWT_REFRESH_SECRET!
      ) as any;

      if (decoded.type !== "refresh") {
        throw new AuthenticationError("Invalid refresh token");
      }

      const user = await dataSources.userAPI.findById(decoded.userId);
      if (!user) {
        throw new AuthenticationError("User not found");
      }

      const tokens = generateTokens(user);

      return {
        ...tokens,
        user,
      };
    } catch (error) {
      throw new AuthenticationError("Invalid refresh token");
    }
  },

  logout: async (_, __, { user, dataSources }) => {
    if (!user) {
      throw new AuthenticationError("Not authenticated");
    }

    // Set user offline
    await dataSources.userAPI.setOffline(user.id);

    return true;
  },

  updateProfile: async (_, { input }, { user, dataSources }) => {
    if (!user) {
      throw new AuthenticationError("Not authenticated");
    }

    // Validate input
    const validatedInput = validateInput(updateUserSchema, input);

    // Check if username is already taken
    if (validatedInput.username) {
      const existingUser = await dataSources.userAPI.findByUsername(
        validatedInput.username
      );
      if (existingUser && existingUser.id !== user.id) {
        throw new UserInputError("Username is already taken");
      }
    }

    // Update user
    const updatedUser = await dataSources.userAPI.update(
      user.id,
      validatedInput
    );

    return updatedUser;
  },

  changePassword: async (
    _,
    { currentPassword, newPassword },
    { user, dataSources }
  ) => {
    if (!user) {
      throw new AuthenticationError("Not authenticated");
    }

    // Get user with password hash
    const userWithPassword = await dataSources.userAPI.findById(user.id, true);
    if (!userWithPassword) {
      throw new Error("User not found");
    }

    // Verify current password
    const isCurrentPasswordValid = await verifyPassword(
      currentPassword,
      userWithPassword.passwordHash
    );
    if (!isCurrentPasswordValid) {
      throw new UserInputError("Current password is incorrect");
    }

    // Validate new password
    if (newPassword.length < 8) {
      throw new UserInputError(
        "New password must be at least 8 characters long"
      );
    }

    // Update password
    const newPasswordHash = await hashPassword(newPassword);
    await dataSources.userAPI.updatePassword(user.id, newPasswordHash);

    return true;
  },

  deleteAccount: async (_, __, { user, dataSources }) => {
    if (!user) {
      throw new AuthenticationError("Not authenticated");
    }

    // Soft delete user
    await dataSources.userAPI.softDelete(user.id);

    return true;
  },

  followUser: async (_, { userId }, { user, dataSources }) => {
    if (!user) {
      throw new AuthenticationError("Not authenticated");
    }

    if (user.id === userId) {
      throw new UserInputError("Cannot follow yourself");
    }

    // Check if target user exists
    const targetUser = await dataSources.userAPI.findById(userId);
    if (!targetUser) {
      throw new UserInputError("User not found");
    }

    // Follow user
    await dataSources.userAPI.follow(user.id, userId);

    // Create notification for followed user
    await dataSources.notificationAPI.create({
      userId: userId,
      type: "FOLLOW",
      title: "New Follower",
      message: `${user.firstName} ${user.lastName} started following you`,
      actionData: {
        followerId: user.id,
      },
    });

    return true;
  },

  unfollowUser: async (_, { userId }, { user, dataSources }) => {
    if (!user) {
      throw new AuthenticationError("Not authenticated");
    }

    if (user.id === userId) {
      throw new UserInputError("Cannot unfollow yourself");
    }

    // Unfollow user
    await dataSources.userAPI.unfollow(user.id, userId);

    return true;
  },
};

// Field resolvers
const User: UserFieldResolvers<GraphQLContext> = {
  fullName: parent => `${parent.firstName} ${parent.lastName}`,

  posts: async (parent, { first = 20, after, orderBy }, { dataSources }) => {
    const limit = Math.min(first, 100);
    const cursor = after ? parseCursor(after) : null;

    const result = await dataSources.postAPI.findMany({
      filters: { authorId: parent.id },
      pagination: { limit, cursor },
      orderBy,
    });

    const edges = result.data.map(post => ({
      node: post,
      cursor: createCursor(post.id),
    }));

    return {
      edges,
      pageInfo: {
        hasNextPage: result.hasNextPage,
        hasPreviousPage: !!after,
        startCursor: edges[0]?.cursor,
        endCursor: edges[edges.length - 1]?.cursor,
      },
      totalCount: result.totalCount,
    };
  },

  followers: async (parent, { first = 20, after }, { dataSources }) => {
    const limit = Math.min(first, 100);
    const cursor = after ? parseCursor(after) : null;

    const result = await dataSources.userAPI.getFollowers(parent.id, {
      limit,
      cursor,
    });

    const edges = result.data.map(follow => ({
      node: follow.follower,
      cursor: createCursor(follow.id),
    }));

    return {
      edges,
      pageInfo: {
        hasNextPage: result.hasNextPage,
        hasPreviousPage: !!after,
        startCursor: edges[0]?.cursor,
        endCursor: edges[edges.length - 1]?.cursor,
      },
      totalCount: result.totalCount,
    };
  },

  following: async (parent, { first = 20, after }, { dataSources }) => {
    const limit = Math.min(first, 100);
    const cursor = after ? parseCursor(after) : null;

    const result = await dataSources.userAPI.getFollowing(parent.id, {
      limit,
      cursor,
    });

    const edges = result.data.map(follow => ({
      node: follow.following,
      cursor: createCursor(follow.id),
    }));

    return {
      edges,
      pageInfo: {
        hasNextPage: result.hasNextPage,
        hasPreviousPage: !!after,
        startCursor: edges[0]?.cursor,
        endCursor: edges[edges.length - 1]?.cursor,
      },
      totalCount: result.totalCount,
    };
  },

  addresses: async (parent, _, { dataSources }) => {
    return dataSources.userAPI.getAddresses(parent.id);
  },

  postsCount: async (parent, _, { dataSources }) => {
    return dataSources.postAPI.countByAuthor(parent.id);
  },

  followersCount: async (parent, _, { dataSources }) => {
    return dataSources.userAPI.getFollowersCount(parent.id);
  },

  followingCount: async (parent, _, { dataSources }) => {
    return dataSources.userAPI.getFollowingCount(parent.id);
  },
};

// Export resolvers
export const userResolvers: Resolvers = {
  Query,
  Mutation,
  User,
};src/resolvers/userResolvers.ts

Step 5: DataLoader Implementation

Create efficient data loading with DataLoader:

import DataLoader from "dataloader";
import { PrismaClient } from "@prisma/client";
import { UserWithRelations, PostWithRelations } from "@/types/prisma";

export class DataLoaders {
  public userById: DataLoader<string, UserWithRelations | null>;
  public postById: DataLoader<string, PostWithRelations | null>;
  public usersByIds: DataLoader<readonly string[], UserWithRelations[]>;
  public postsByAuthorId: DataLoader<string, PostWithRelations[]>;
  public followersCountByUserId: DataLoader<string, number>;
  public followingCountByUserId: DataLoader<string, number>;

  constructor(private prisma: PrismaClient) {
    // User by ID loader
    this.userById = new DataLoader(
      async (userIds: readonly string[]) => {
        const users = await this.prisma.user.findMany({
          where: {
            id: { in: [...userIds] },
            deletedAt: null,
          },
          include: {
            addresses: true,
            _count: {
              select: {
                posts: true,
                followers: true,
                follows: true,
              },
            },
          },
        });

        const userMap = new Map(users.map(user => [user.id, user]));

        return userIds.map(id => userMap.get(id) || null);
      },
      {
        cacheKeyFn: key => `user:${key}`,
        maxBatchSize: 100,
      }
    );

    // Post by ID loader
    this.postById = new DataLoader(
      async (postIds: readonly string[]) => {
        const posts = await this.prisma.post.findMany({
          where: {
            id: { in: [...postIds] },
            deletedAt: null,
          },
          include: {
            author: {
              select: {
                id: true,
                username: true,
                firstName: true,
                lastName: true,
                avatar: true,
              },
            },
            tags: true,
            _count: {
              select: {
                comments: { where: { status: "APPROVED" } },
                likes: true,
              },
            },
          },
        });

        const postMap = new Map(posts.map(post => [post.id, post]));

        return postIds.map(id => postMap.get(id) || null);
      },
      {
        cacheKeyFn: key => `post:${key}`,
        maxBatchSize: 100,
      }
    );

    // Multiple users by IDs loader
    this.usersByIds = new DataLoader(
      async (userIdArrays: readonly (readonly string[])[]) => {
        const allUserIds = [...new Set(userIdArrays.flat())];

        const users = await this.prisma.user.findMany({
          where: {
            id: { in: allUserIds },
            deletedAt: null,
          },
          include: {
            addresses: true,
          },
        });

        const userMap = new Map(users.map(user => [user.id, user]));

        return userIdArrays.map(
          userIds =>
            userIds
              .map(id => userMap.get(id))
              .filter(Boolean) as UserWithRelations[]
        );
      },
      {
        cacheKeyFn: key => `users:${key.join(",")}`,
      }
    );

    // Posts by author ID loader
    this.postsByAuthorId = new DataLoader(
      async (authorIds: readonly string[]) => {
        const posts = await this.prisma.post.findMany({
          where: {
            authorId: { in: [...authorIds] },
            deletedAt: null,
          },
          include: {
            author: {
              select: {
                id: true,
                username: true,
                firstName: true,
                lastName: true,
                avatar: true,
              },
            },
            tags: true,
            _count: {
              select: {
                comments: { where: { status: "APPROVED" } },
                likes: true,
              },
            },
          },
          orderBy: { createdAt: "desc" },
        });

        const postsByAuthor = new Map<string, PostWithRelations[]>();

        posts.forEach(post => {
          const authorPosts = postsByAuthor.get(post.authorId) || [];
          authorPosts.push(post);
          postsByAuthor.set(post.authorId, authorPosts);
        });

        return authorIds.map(authorId => postsByAuthor.get(authorId) || []);
      },
      {
        cacheKeyFn: key => `posts_by_author:${key}`,
      }
    );

    // Followers count by user ID loader
    this.followersCountByUserId = new DataLoader(
      async (userIds: readonly string[]) => {
        const counts = await this.prisma.follow.groupBy({
          by: ["followingId"],
          where: {
            followingId: { in: [...userIds] },
          },
          _count: {
            followerId: true,
          },
        });

        const countMap = new Map(
          counts.map(count => [count.followingId, count._count.followerId])
        );

        return userIds.map(id => countMap.get(id) || 0);
      },
      {
        cacheKeyFn: key => `followers_count:${key}`,
      }
    );

    // Following count by user ID loader
    this.followingCountByUserId = new DataLoader(
      async (userIds: readonly string[]) => {
        const counts = await this.prisma.follow.groupBy({
          by: ["followerId"],
          where: {
            followerId: { in: [...userIds] },
          },
          _count: {
            followingId: true,
          },
        });

        const countMap = new Map(
          counts.map(count => [count.followerId, count._count.followingId])
        );

        return userIds.map(id => countMap.get(id) || 0);
      },
      {
        cacheKeyFn: key => `following_count:${key}`,
      }
    );
  }

  // Clear all caches
  clearAll(): void {
    this.userById.clearAll();
    this.postById.clearAll();
    this.usersByIds.clearAll();
    this.postsByAuthorId.clearAll();
    this.followersCountByUserId.clearAll();
    this.followingCountByUserId.clearAll();
  }

  // Clear cache for specific user
  clearUser(userId: string): void {
    this.userById.clear(userId);
    this.postsByAuthorId.clear(userId);
    this.followersCountByUserId.clear(userId);
    this.followingCountByUserId.clear(userId);
  }

  // Clear cache for specific post
  clearPost(postId: string): void {
    this.postById.clear(postId);
  }
}

export const createDataLoaders = (prisma: PrismaClient) =>
  new DataLoaders(prisma);src/dataloaders/index.ts

Best Practices Summary

  1. Schema Design: Use schema-first approach with comprehensive type definitions
  2. Code Generation: Leverage GraphQL Code Generator for type safety
  3. Resolvers: Implement efficient resolvers with proper error handling
  4. DataLoader: Use DataLoader to prevent N+1 query problems
  5. Authentication: Implement proper JWT authentication and authorization
  6. Validation: Use Zod or similar libraries for input validation
  7. Performance: Optimize queries with field-level caching and batching
  8. Security: Implement rate limiting, depth limiting, and query complexity analysis
  9. Testing: Write comprehensive tests for resolvers and business logic
  10. Documentation: Maintain clear schema documentation and examples

Development Commands

# Generate types
npm run codegen

# Start development server
npm run dev

# Build for production
npm run build

# Run tests
npm test

# Lint code
npm run lint

Your GraphQL TypeScript API is now ready for production with comprehensive schema design, type safety, efficient data loading, and modern GraphQL features!


Share this post on:

Previous Post
Next.js 15 TypeScript: Full-Stack Web Application Development
Next Post
Dokploy Deployment Automation: Self-Hosted DevOps Platform