Techdots

July 14, 2025

Type-Safe API Development in Node.js with TypeScript and GraphQL

Type-Safe API Development in Node.js with TypeScript and GraphQL

Are you tired of runtime errors breaking your API? Do you want to build APIs that are both reliable and easy to maintain? If you're working with Node.js and want to create better APIs, you've come to the right place.

Building APIs can be tricky. One small mistake in your code can cause errors that only show up when your app is running. This is where TypeScript and GraphQL come to the rescue. They help you catch errors before they happen and make your code much safer.

In this guide, we'll show you how to build type-safe APIs using Node.js, TypeScript, and GraphQL. You'll learn simple techniques that will make your APIs more reliable and your development process smoother.

Why Type Safety is Important for Your API

Think of type safety as a safety net for your code. When you use static typing, you're telling your code exactly what kind of data it should expect. This means:

  • Fewer runtime errors: Problems get caught before your app goes live
  • Better code quality: Your code becomes more predictable and easier to understand
  • Faster development: You spend less time debugging and more time building features
  • Improved API security: Invalid data gets blocked before it can cause problems

When you combine TypeScript with GraphQL, you get type safety at every level of your API. This is much better than traditional REST API approaches where type checking happens manually.

Setting Up Your Project

Let's start by creating a new Node.js project with TypeScript and GraphQL. Don't worry - it's easier than it sounds!

Install Required Packages


import 'reflect-metadata';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { Resolver, Query, Mutation, Arg, Field, ObjectType, ID, InputType } from 'type-graphql';

// --- Types and Inputs ---

@ObjectType()
class User {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field()
  email: string;
}

@InputType()
class CreateUserInput {
  @Field()
  name: string;

  @Field()
  email: string;
}

// --- Resolver ---

@Resolver()
class UserResolver {
  private users: User[] = [];

  @Query(() => [User])
  getUsers(): User[] {
    return this.users;
  }

  @Mutation(() => User)
  createUser(@Arg('input') input: CreateUserInput): User {
    const user = { id: String(this.users.length + 1), ...input };
    this.users.push(user);
    return user;
  }
}

// --- Server Bootstrap ---

async function bootstrap() {
  const schema = await buildSchema({
    resolvers: [UserResolver],
  });

  const server = new ApolloServer({ schema });
  await server.start();

  const app = express();
  server.applyMiddleware({ app });

  app.listen(4000, () => {
    console.log('πŸš€ Server ready at http://localhost:4000/graphql');
  });
}

bootstrap();

  

Configure TypeScript

Create a tsconfig.json file in your project root:


{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}
  

Creating Your GraphQL Schema with TypeScript

Instead of writing separate schema files, we'll use TypeScript classes to define our GraphQL schema. This approach keeps everything in sync automatically.

Example: Building a User Schema


import { ObjectType, Field, ID, Resolver, Query, Arg } from 'type-graphql';

@ObjectType()
class User {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field()
  email: string;
}

@Resolver()
class UserResolver {
  private users: User[] = [
    { id: '1', name: 'Alice', email: 'alice@example.com' },
    { id: '2', name: 'Bob', email: 'bob@example.com' },
  ];

  @Query(() => [User])
  getUsers(): User[] {
    return this.users;
  }

  @Query(() => User, { nullable: true })
  getUser(@Arg('id') id: string): User | undefined {
    return this.users.find(user => user.id === id);
  }
}

  

Why this approach works so well:

  • Type safety: Your schema and TypeScript types are always in sync
  • Code reusability: Use the same types throughout your application
  • Schema validation: GraphQL automatically validates incoming data

Setting Up Apollo Server

Apollo Server is a popular GraphQL server that works perfectly with TypeScript. Here's how to set it up:


import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { UserResolver } from './resolvers/UserResolver';

async function startServer() {
  const app = express();

  const schema = await buildSchema({
    resolvers: [UserResolver],
  });

  const server = new ApolloServer({ schema });

  await server.start();
  server.applyMiddleware({ app });

  app.listen({ port: 4000 }, () => {
    console.log(`πŸš€ Server ready at http://localhost:4000${server.graphqlPath}`);
  });
}

startServer(); // ← Make sure to call the function!

  

This setup gives you a fully functional GraphQL server with runtime safety built-in.

Automatic Code Generation

One of the best parts about this setup is automatic code generation. You can generate TypeScript types directly from your GraphQL schema, ensuring everything stays in sync.

Install Code Generation Tools


npm install --save-dev \
  @graphql-codegen/cli \
  @graphql-codegen/typescript \
  @graphql-codegen/typescript-resolvers
  

Configure Code Generation

Create a codegen.yml file:


schema: ./src/schema.graphql
generates:
  ./src/generated/types.ts:
    plugins:
      - typescript
      - typescript-resolvers
  

Generate Types

Run this command to generate TypeScript types:


npx graphql-codegen
  

Using Generated Types


import { ObjectType, Field, ID, Resolver, Query } from 'type-graphql';
import { User as UserType } from './generated/types';

@ObjectType()
class User implements UserType {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field()
  email: string;
}

@Resolver()
class UserResolver {
  @Query(() => [User])
  getUsers(): User[] {
    // Use actual DB logic here
    return [
      { id: '1', name: 'Test', email: 'test@example.com' }
    ];
  }
}

  

Implementing Strong Error Handling

Good error handling is crucial for any API. With TypeScript and GraphQL, you can create consistent, type-safe error handling.

Example: Custom Error Handling


import { Resolver, Query, Arg } from 'type-graphql';
import { ApolloError } from 'apollo-server-express';
import { User } from '../models/User'; // Adjust path to your decorated User class

@Resolver()
export class UserResolver {
  @Query(() => User)
  async getUser(@Arg('id') id: string): Promise {
    const user = await fetchUserFromDatabase(id);

    if (!user) {
      throw new ApolloError('User not found', 'USER_NOT_FOUND');
    }

    return user;
  }
}

// Example mock implementation β€” replace with real DB call
async function fetchUserFromDatabase(id: string): Promise {
  const mockUsers: User[] = [
    { id: '1', name: 'Alice', email: 'alice@example.com' },
    { id: '2', name: 'Bob', email: 'bob@example.com' }
  ];

  return mockUsers.find(user => user.id === id) || null;
}

  

Benefits of this approach:

  • Consistency: All errors follow the same pattern
  • Type safety: Errors are predictable and typed
  • Better debugging: Clear error messages help you fix problems faster

Building Secure APIs

API security is essential, and TypeScript helps you build secure APIs by validating inputs and enforcing security rules.

Input Validation Example


import { InputType, Field } from 'type-graphql';
import { MinLength, MaxLength, IsEmail } from 'class-validator';

@InputType()
export class CreateUserInput {
  @Field()
  @MinLength(3)
  @MaxLength(50)
  name: string;

  @Field()
  @IsEmail()
  email: string;
}
  

Authentication Middleware


import { ApolloError } from 'apollo-server-express';
import { Context } from './context';

export const isAuthenticated = ({ context }: { context: Context }) => {
  if (!context.user) {
    throw new ApolloError('Unauthorized', 'UNAUTHORIZED');
  }
};
  

Complete Example: Building a User Management API

Let's put everything together with a complete example:

Schema Definition


type User {
  id: ID!
  name: String!
  email: String!
}
type Query {
  users: [User!]!
  user(id: ID!): User
}
type Mutation {
  createUser(name: String!, email: String!): User!
}
  

Resolvers


import { Resolver, Query, Mutation, Arg } from 'type-graphql';
import { User } from '../models/User'; // Adjust path as needed
import { CreateUserInput } from '../inputs/CreateUserInput'; // Adjust path as needed
@Resolver()
export class UserResolver {
  private users: User[] = [];
  @Query(() => [User])
  getUsers(): User[] {
    return this.users;
  }
  @Mutation(() => User)
  createUser(@Arg('input') input: CreateUserInput): User {
    const user: User = {
      id: String(this.users.length + 1),
      ...input
    };
    this.users.push(user);
    return user;
  }
}
  

This example shows how all the pieces work together to create a type-safe, secure API.

Conclusion

TypeScript and GraphQL make building reliable APIs much easier than traditional REST API methods.Β 

With static typing, automatic code generation, and built-in schema validation, you can catch errors early and build more secure applications. These tools help you create APIs that are both powerful and maintainable.

Ready to build better APIs? Contact TechDots today and let our expert team help you create type-safe, scalable solutions for your business needs.

‍

Ready to Launch Your AI MVP with Techdots?

Techdots has helped 15+ founders transform their visions into market-ready AI products. Each started exactly where you are now - with an idea and the courage to act on it.

Techdots: Where Founder Vision Meets AI Reality

Book Meeting