Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

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

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
How to sort an array of objects by string property value in JavaScript
How to sort an array of objects by string property value in JavaScript

How to loop through a 2D array in JavaScript
How to loop through a 2D array in JavaScript

What Does javascript:void(0) Mean?
What Does javascript:void(0) Mean?

How to change opacity on hover in CSS
How to change opacity on hover in CSS

Answers by CoreUI Core Team