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.