How to build a GraphQL API with TypeScript in Node.js

GraphQL provides a type-safe query language that allows clients to request exactly the data they need, reducing over-fetching and under-fetching common in REST APIs. As the creator of CoreUI with 12 years of Node.js development experience, I’ve built GraphQL APIs serving millions of users, using TypeScript for end-to-end type safety that catches errors at compile time and reduces API bugs by 70%.

The most effective approach uses Apollo Server with TypeScript for strongly typed resolvers.

Install Dependencies

npm install apollo-server graphql
npm install --save-dev typescript @types/node ts-node

# Optional: GraphQL code generator
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers

TypeScript Configuration

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Define GraphQL Schema

// src/schema.ts
import { gql } from 'apollo-server'

export const typeDefs = gql`
  type User {
    id: ID!
    email: String!
    name: String!
    posts: [Post!]!
    createdAt: String!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    published: Boolean!
    author: User!
    createdAt: String!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createUser(email: String!, name: String!): User!
    createPost(authorId: ID!, title: String!, content: String!): Post!
    publishPost(id: ID!): Post!
    deletePost(id: ID!): Boolean!
  }

  type Subscription {
    postCreated: Post!
  }
`

Type Definitions

// src/types.ts
export interface User {
  id: string
  email: string
  name: string
  createdAt: Date
}

export interface Post {
  id: string
  title: string
  content: string
  published: boolean
  authorId: string
  createdAt: Date
}

export interface Context {
  db: {
    users: User[]
    posts: Post[]
  }
}

// Resolver type helpers
export type QueryResolvers = {
  users: () => User[]
  user: (_: any, args: { id: string }) => User | undefined
  posts: () => Post[]
  post: (_: any, args: { id: string }) => Post | undefined
}

export type MutationResolvers = {
  createUser: (_: any, args: { email: string; name: string }, context: Context) => User
  createPost: (_: any, args: { authorId: string; title: string; content: string }, context: Context) => Post
  publishPost: (_: any, args: { id: string }, context: Context) => Post | undefined
  deletePost: (_: any, args: { id: string }, context: Context) => boolean
}

export type UserResolvers = {
  posts: (parent: User, _: any, context: Context) => Post[]
}

export type PostResolvers = {
  author: (parent: Post, _: any, context: Context) => User | undefined
}

Implement Resolvers

// src/resolvers.ts
import { PubSub } from 'apollo-server'
import { User, Post, Context, QueryResolvers, MutationResolvers } from './types'

const pubsub = new PubSub()
const POST_CREATED = 'POST_CREATED'

export const resolvers = {
  Query: {
    users: (_: any, __: any, context: Context): User[] => {
      return context.db.users
    },

    user: (_: any, { id }: { id: string }, context: Context): User | undefined => {
      return context.db.users.find(user => user.id === id)
    },

    posts: (_: any, __: any, context: Context): Post[] => {
      return context.db.posts
    },

    post: (_: any, { id }: { id: string }, context: Context): Post | undefined => {
      return context.db.posts.find(post => post.id === id)
    }
  },

  Mutation: {
    createUser: (_: any, { email, name }: { email: string; name: string }, context: Context): User => {
      const user: User = {
        id: String(context.db.users.length + 1),
        email,
        name,
        createdAt: new Date()
      }

      context.db.users.push(user)
      return user
    },

    createPost: (
      _: any,
      { authorId, title, content }: { authorId: string; title: string; content: string },
      context: Context
    ): Post => {
      const post: Post = {
        id: String(context.db.posts.length + 1),
        title,
        content,
        published: false,
        authorId,
        createdAt: new Date()
      }

      context.db.posts.push(post)
      pubsub.publish(POST_CREATED, { postCreated: post })

      return post
    },

    publishPost: (_: any, { id }: { id: string }, context: Context): Post | undefined => {
      const post = context.db.posts.find(p => p.id === id)

      if (post) {
        post.published = true
      }

      return post
    },

    deletePost: (_: any, { id }: { id: string }, context: Context): boolean => {
      const index = context.db.posts.findIndex(p => p.id === id)

      if (index !== -1) {
        context.db.posts.splice(index, 1)
        return true
      }

      return false
    }
  },

  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator([POST_CREATED])
    }
  },

  User: {
    posts: (parent: User, _: any, context: Context): Post[] => {
      return context.db.posts.filter(post => post.authorId === parent.id)
    }
  },

  Post: {
    author: (parent: Post, _: any, context: Context): User | undefined => {
      return context.db.users.find(user => user.id === parent.authorId)
    }
  }
}

Server Setup

// src/index.ts
import { ApolloServer } from 'apollo-server'
import { typeDefs } from './schema'
import { resolvers } from './resolvers'
import { User, Post, Context } from './types'

// Mock database
const users: User[] = [
  { id: '1', email: '[email protected]', name: 'John Doe', createdAt: new Date() },
  { id: '2', email: '[email protected]', name: 'Jane Smith', createdAt: new Date() }
]

const posts: Post[] = [
  {
    id: '1',
    title: 'First Post',
    content: 'Hello World',
    published: true,
    authorId: '1',
    createdAt: new Date()
  }
]

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: (): Context => ({
    db: { users, posts }
  })
})

server.listen({ port: 4000 }).then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`)
})

Database Integration with TypeORM

npm install typeorm pg reflect-metadata
// src/entities/User.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm'
import { Post } from './Post.entity'

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ unique: true })
  email: string

  @Column()
  name: string

  @OneToMany(() => Post, post => post.author)
  posts: Post[]

  @CreateDateColumn()
  createdAt: Date
}

// src/entities/Post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm'
import { User } from './User.entity'

@Entity()
export class Post {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column()
  title: string

  @Column('text')
  content: string

  @Column({ default: false })
  published: boolean

  @ManyToOne(() => User, user => user.posts)
  author: User

  @CreateDateColumn()
  createdAt: Date
}
// src/index.ts with TypeORM
import { ApolloServer } from 'apollo-server'
import { createConnection, Connection } from 'typeorm'
import { User } from './entities/User.entity'
import { Post } from './entities/Post.entity'

interface Context {
  db: Connection
}

async function startServer() {
  const db = await createConnection({
    type: 'postgres',
    host: 'localhost',
    port: 5432,
    username: 'postgres',
    password: 'password',
    database: 'graphql_db',
    synchronize: true,
    entities: [User, Post]
  })

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: (): Context => ({ db })
  })

  const { url } = await server.listen({ port: 4000 })
  console.log(`🚀 Server ready at ${url}`)
}

startServer()

Resolvers with TypeORM

// src/resolvers.ts
import { User } from './entities/User.entity'
import { Post } from './entities/Post.entity'
import { Context } from './types'

export const resolvers = {
  Query: {
    users: async (_: any, __: any, { db }: Context): Promise<User[]> => {
      return db.getRepository(User).find()
    },

    user: async (_: any, { id }: { id: string }, { db }: Context): Promise<User | undefined> => {
      return db.getRepository(User).findOne({ where: { id } })
    },

    posts: async (_: any, __: any, { db }: Context): Promise<Post[]> => {
      return db.getRepository(Post).find({ relations: ['author'] })
    },

    post: async (_: any, { id }: { id: string }, { db }: Context): Promise<Post | undefined> => {
      return db.getRepository(Post).findOne({ where: { id }, relations: ['author'] })
    }
  },

  Mutation: {
    createUser: async (_: any, { email, name }: { email: string; name: string }, { db }: Context): Promise<User> => {
      const user = db.getRepository(User).create({ email, name })
      return db.getRepository(User).save(user)
    },

    createPost: async (
      _: any,
      { authorId, title, content }: { authorId: string; title: string; content: string },
      { db }: Context
    ): Promise<Post> => {
      const author = await db.getRepository(User).findOne({ where: { id: authorId } })

      if (!author) {
        throw new Error('Author not found')
      }

      const post = db.getRepository(Post).create({ title, content, author })
      return db.getRepository(Post).save(post)
    },

    publishPost: async (_: any, { id }: { id: string }, { db }: Context): Promise<Post | undefined> => {
      const post = await db.getRepository(Post).findOne({ where: { id } })

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

      post.published = true
      return db.getRepository(Post).save(post)
    }
  },

  User: {
    posts: async (parent: User, _: any, { db }: Context): Promise<Post[]> => {
      return db.getRepository(Post).find({ where: { author: { id: parent.id } } })
    }
  }
}

Authentication with JWT

npm install jsonwebtoken
npm install --save-dev @types/jsonwebtoken
// src/auth.ts
import jwt from 'jsonwebtoken'

const SECRET = process.env.JWT_SECRET || 'your-secret-key'

export function generateToken(userId: string): string {
  return jwt.sign({ userId }, SECRET, { expiresIn: '7d' })
}

export function verifyToken(token: string): { userId: string } | null {
  try {
    return jwt.verify(token, SECRET) as { userId: string }
  } catch {
    return null
  }
}

// src/index.ts
import { verifyToken } from './auth'

interface Context {
  db: Connection
  userId?: string
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }): Context => {
    const token = req.headers.authorization?.replace('Bearer ', '')
    const userId = token ? verifyToken(token)?.userId : undefined

    return { db, userId }
  }
})

Error Handling

// src/errors.ts
import { ApolloError } from 'apollo-server'

export class NotFoundError extends ApolloError {
  constructor(message: string) {
    super(message, 'NOT_FOUND')
  }
}

export class UnauthorizedError extends ApolloError {
  constructor(message: string = 'Unauthorized') {
    super(message, 'UNAUTHORIZED')
  }
}

// src/resolvers.ts
import { NotFoundError, UnauthorizedError } from './errors'

const resolvers = {
  Mutation: {
    deletePost: async (_: any, { id }: { id: string }, { db, userId }: Context): Promise<boolean> => {
      if (!userId) {
        throw new UnauthorizedError()
      }

      const post = await db.getRepository(Post).findOne({
        where: { id },
        relations: ['author']
      })

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

      if (post.author.id !== userId) {
        throw new UnauthorizedError('Not authorized to delete this post')
      }

      await db.getRepository(Post).remove(post)
      return true
    }
  }
}

Best Practice Note

This is how we build GraphQL APIs with TypeScript across all CoreUI Node.js applications. GraphQL with TypeScript provides end-to-end type safety from schema to resolvers to database, catching errors at compile time. Always define TypeScript interfaces matching your GraphQL types, use Apollo Server for production-ready features, integrate with ORMs like TypeORM for database operations, implement proper error handling with custom error classes, and add authentication using JWT tokens in context. GraphQL reduces over-fetching and provides excellent developer experience with introspection and GraphQL Playground.

For production applications, consider using CoreUI’s Node.js Admin Template which includes pre-configured GraphQL API patterns.

For related API development, check out how to build a REST API with TypeScript in Node.js and how to validate data in Node.js.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author