I’ve been thinking a lot about how we build APIs that are both powerful and safe. The combination of TypeScript, GraphQL, and modern ORMs offers something special: complete type safety from database to frontend. That’s why I want to share this practical approach using TypeGraphQL, Apollo Server, and Prisma.
Let me show you how to build something robust. First, set up your project structure. The key is keeping your code organized from the start.
npm init -y
npm install apollo-server-express type-graphql @prisma/client prisma
npm install -D typescript @types/node
Have you considered how your database schema affects your entire application? Prisma makes this intuitive. Here’s a simple user model to begin with:
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
Now, let’s create a corresponding TypeGraphQL entity. Notice how the types align perfectly:
import { ObjectType, Field, ID } from 'type-graphql';
@ObjectType()
export class User {
@Field(() => ID)
id: string;
@Field()
email: string;
@Field({ nullable: true })
name?: string;
@Field()
createdAt: Date;
}
What happens when you need to query this data? That’s where resolvers come in. Here’s a basic user resolver:
import { Query, Resolver } from 'type-graphql';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
@Resolver()
export class UserResolver {
@Query(() => [User])
async users() {
return prisma.user.findMany();
}
}
But what about mutations? Let’s create a user with proper validation:
import { Arg, Mutation, Resolver } from 'type-graphql';
import { IsEmail } from 'class-validator';
class CreateUserInput {
@Field()
@IsEmail()
email: string;
@Field({ nullable: true })
name?: string;
}
@Resolver()
export class UserResolver {
@Mutation(() => User)
async createUser(@Arg('data') data: CreateUserInput) {
return prisma.user.create({ data });
}
}
Authentication is crucial for most applications. Here’s a simple way to protect a resolver:
import { UseMiddleware } from 'type-graphql';
import { isAuth } from './middleware/isAuth';
@Resolver()
export class ProtectedResolver {
@Query(() => String)
@UseMiddleware(isAuth)
async secretData() {
return "This is protected information";
}
}
Performance matters too. Have you thought about how to avoid the N+1 query problem? DataLoader is your friend:
import DataLoader from 'dataloader';
const createUserLoader = () => {
return new DataLoader(async (userIds: string[]) => {
const users = await prisma.user.findMany({
where: { id: { in: userIds } }
});
return userIds.map(id => users.find(user => user.id === id));
});
};
Error handling should be consistent across your API. Here’s a pattern I find useful:
import { ApolloError } from 'apollo-server-express';
@Resolver()
export class UserResolver {
@Mutation(() => User)
async createUser(@Arg('data') data: CreateUserInput) {
try {
return await prisma.user.create({ data });
} catch (error) {
if (error.code === 'P2002') {
throw new ApolloError('User already exists');
}
throw error;
}
}
}
Testing your GraphQL API doesn’t have to be complicated. Here’s a simple test setup:
import { createTestClient } from 'apollo-server-testing';
import { ApolloServer } from 'apollo-server-express';
const testServer = new ApolloServer({
schema: await buildSchema({
resolvers: [UserResolver]
})
});
const { query, mutate } = createTestClient(testServer);
The beauty of this stack is how everything connects. Your Prisma models inform your GraphQL schema, which in turn shapes your frontend queries. It’s a complete type-safe journey.
What challenges have you faced when building APIs? I’d love to hear about your experiences in the comments below. If you found this helpful, please share it with others who might benefit from these patterns.
Remember, the goal isn’t just writing code—it’s creating maintainable, scalable solutions that stand the test of time. Every decision you make about structure and validation pays dividends later.
I hope this gives you a solid foundation to build upon. The combination of TypeGraphQL’s decorators, Prisma’s type-safe client, and Apollo Server’s robustness creates a development experience that’s both productive and safe.
What would you add to this setup? Share your thoughts and let’s continue the conversation. Don’t forget to like and share if this resonated with you!