I’ve been building APIs for years, and I keep seeing the same pattern: developers start with great intentions for type safety, but as projects grow, things get messy. GraphQL promised a better way, but without the right tools, it can become a maintenance nightmare. That’s why I decided to combine NestJS, Prisma, and code-first schema generation—to create APIs where types flow seamlessly from database to frontend. If you’ve ever spent hours debugging type mismatches or wrestling with schema conflicts, you’ll appreciate this approach.
Let me show you how to set up a robust foundation. Start by creating a new NestJS project and installing the essential packages. I prefer using the NestJS CLI because it handles the boilerplate perfectly.
nest new graphql-api
cd graphql-api
npm install @nestjs/graphql @nestjs/apollo graphql prisma @prisma/client
Did you know that the code-first approach lets your TypeScript definitions automatically generate your GraphQL schema? This means less duplication and fewer errors. Here’s how I configure the GraphQL module in the main app module.
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
buildSchemaOptions: {
dateScalarMode: 'timestamp',
},
}),
],
})
export class AppModule {}
Now, let’s design our database schema with Prisma. I use PostgreSQL for its reliability and JSON support. The Prisma schema acts as our single source of truth for database structure.
model User {
id String @id @default(cuid())
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
content String
author User @relation(fields: [authorId], references: [id])
authorId String
}
After defining models, run npx prisma generate
to create the Prisma Client. This gives us fully typed database operations. How often have you wished for autocompletion when writing database queries? With Prisma, that’s the default experience.
Next, I create GraphQL object types using NestJS decorators. These classes serve dual purposes: they’re both our GraphQL types and DTOs for validation.
@ObjectType()
export class User {
@Field(() => ID)
id: string;
@Field()
email: string;
@Field(() => [Post])
posts?: Post[];
}
@InputType()
export class CreateUserInput {
@Field()
@IsEmail()
email: string;
}
Notice how I’m using class-validator decorators alongside GraphQL decorators? This ensures input validation happens automatically before data reaches our resolvers. What if you need to compute fields on the fly? That’s where field resolvers shine.
Here’s a user resolver with a custom field that calculates post count without storing it in the database.
@Resolver(() => User)
export class UsersResolver {
constructor(private prisma: PrismaService) {}
@Query(() => [User])
async users() {
return this.prisma.user.findMany();
}
@FieldResolver()
async postCount(@Parent() user: User) {
const { id } = user;
return this.prisma.post.count({ where: { authorId: id } });
}
}
Authentication is crucial for any API. I implement it using GraphQL guards and JWT tokens. The beauty of NestJS is how cleanly these integrate.
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
Then protect your queries by adding the guard.
@Query(() => User)
@UseGuards(GqlAuthGuard)
async me(@CurrentUser() user: User) {
return user;
}
Testing might not be glamorous, but it’s essential. I write comprehensive tests using Jest and Supertest to verify everything works as expected.
describe('UsersResolver', () => {
let resolver: UsersResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersResolver, PrismaService],
}).compile();
resolver = module.get<UsersResolver>(UsersResolver);
});
it('should return users', async () => {
const result = await resolver.users();
expect(result).toBeInstanceOf(Array);
});
});
When deploying, I optimize performance by implementing dataloader for N+1 query issues and adding query complexity limits. Have you considered how much data a single GraphQL query might fetch? It’s worth monitoring in production.
This approach has transformed how I build APIs. The type safety from database to frontend reduces bugs and speeds up development. I can refactor with confidence, knowing that TypeScript will catch breaking changes early.
What challenges have you faced with GraphQL APIs? Share your experiences in the comments below—I’d love to hear how you handle type safety in your projects. If this guide helped you, please like and share it with other developers who might benefit. Let’s build better APIs together!