Building a GraphQL API with Node.js and TypeScript

Why GraphQL?

REST APIs are great, but GraphQL takes API development to the next level. It lets clients request exactly the data they need, reduces over-fetching, and provides a strongly-typed schema. Combined with TypeScript, you get end-to-end type safety and amazing developer experience.

Interactive Demo

Try out this interactive demo to see how GraphQL queries work. You can execute different queries and see the responses:

Query

Response

# Response will appear here

Project Setup

First, let's create a new project and install our dependencies:

mkdir graphql-api
cd graphql-api
npm init -y
npm install @apollo/server graphql graphql-tag
npm install --save-dev typescript @types/node ts-node-dev

Configure TypeScript by creating a tsconfig.json:

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Type Definitions

Create src/types.ts to define our data types:

export interface User {
  id: string;
  name: string;
  email: string;
  posts: string[];
}

export interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
}

Schema Definition

Create src/schema.ts to define our GraphQL schema:

import { gql } from 'graphql-tag';

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

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

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

  type Mutation {
    createUser(name: String!, email: String!): User!
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }
`;

Resolvers

Create src/resolvers.ts to implement our resolver functions:

import { User, Post } from './types';

// Mock database
const db = {
  users: [
    { id: '1', name: 'John Doe', email: 'john@example.com', posts: ['1', '2'] },
    { id: '2', name: 'Jane Smith', email: 'jane@example.com', posts: ['3'] },
  ],
  posts: [
    { id: '1', title: 'GraphQL Basics', content: 'Introduction to GraphQL...', authorId: '1' },
    { id: '2', title: 'Advanced Queries', content: 'Deep dive into GraphQL...', authorId: '1' },
    { id: '3', title: 'TypeScript Tips', content: 'Best practices for TypeScript...', authorId: '2' },
  ],
};

export const resolvers = {
  Query: {
    users: () => db.users,
    user: (_: never, { id }: { id: string }) => 
      db.users.find(u => u.id === id),
    posts: () => db.posts,
    post: (_: never, { id }: { id: string }) => 
      db.posts.find(p => p.id === id),
  },
  Mutation: {
    createUser: (_: never, { name, email }: { name: string, email: string }) => {
      const user: User = {
        id: String(db.users.length + 1),
        name,
        email,
        posts: [],
      };
      db.users.push(user);
      return user;
    },
    createPost: (_: never, { title, content, authorId }: { title: string, content: string, authorId: string }) => {
      const post: Post = {
        id: String(db.posts.length + 1),
        title,
        content,
        authorId,
      };
      db.posts.push(post);
      const user = db.users.find(u => u.id === authorId);
      if (user) {
        user.posts.push(post.id);
      }
      return post;
    },
  },
  User: {
    posts: (parent: User) => 
      parent.posts.map(id => db.posts.find(p => p.id === id)),
  },
  Post: {
    author: (parent: Post) => 
      db.users.find(u => u.id === parent.authorId),
  },
};

Server Setup

Create src/index.ts to set up our Apollo Server:

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';

async function startServer() {
  const server = new ApolloServer({
    typeDefs,
    resolvers,
  });

  const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
  });

  console.log(`🚀 Server ready at ${url}`);
}

startServer().catch(console.error);

Running the Server

Add these scripts to your package.json:

{
  "scripts": {
    "dev": "ts-node-dev --respawn src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Now you can test your API with these example queries:

# Get all users with their posts
query {
  users {
    id
    name
    email
    posts {
      id
      title
    }
  }
}

# Create a new user
mutation {
  createUser(name: "Alice", email: "alice@example.com") {
    id
    name
    email
  }
}

# Create a post
mutation {
  createPost(
    title: "GraphQL is Awesome"
    content: "Here's why..."
    authorId: "1"
  ) {
    id
    title
    author {
      name
    }
  }
}

Best Practices

1. Schema Design

  • Use clear, descriptive type and field names
  • Keep mutations focused and atomic
  • Use input types for complex mutations
  • Consider pagination for lists

2. Type Safety

  • Use TypeScript interfaces that match your GraphQL types
  • Generate TypeScript types from your schema
  • Use nullability carefully
  • Validate input data

3. Performance

  • Implement DataLoader for batching and caching
  • Use field-level resolvers wisely
  • Consider query complexity analysis
  • Implement proper error handling

Error Handling

Add proper error handling to your resolvers:

import { GraphQLError } from 'graphql';

// In your resolver
user: async (_, { id }) => {
  try {
    const user = await db.users.find(u => u.id === id);
    if (!user) {
      throw new GraphQLError('User not found', {
        extensions: { code: 'NOT_FOUND' },
      });
    }
    return user;
  } catch (error) {
    throw new GraphQLError('Failed to fetch user', {
      extensions: { code: 'INTERNAL_SERVER_ERROR' },
    });
  }
}

Authentication

Add authentication using context:

// Add to your server setup
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    const token = req.headers.authorization || '';
    const user = await validateToken(token);
    return { user };
  },
});

// In your resolvers
createPost: async (_, args, context) => {
  if (!context.user) {
    throw new GraphQLError('Not authenticated', {
      extensions: { code: 'UNAUTHENTICATED' },
    });
  }
  // Create post...
}

Next Steps

To make your GraphQL API production-ready, consider adding:

  • Database integration (e.g., Prisma)
  • Authentication and authorization
  • Input validation
  • Rate limiting
  • Monitoring and logging
  • API documentation
  • Testing

Pro Tip: Consider using code-generation tools like GraphQL Code Generator to automatically generate TypeScript types from your schema. This ensures perfect type safety between your schema and resolvers.