I’ve been building APIs for years, but recently I noticed something interesting. Many developers struggle to move from basic GraphQL implementations to production-ready systems. That’s why I want to share my approach to creating robust GraphQL APIs using NestJS, Prisma, and Redis. These tools work together beautifully to handle real-world demands.
Let me show you how to set up the foundation. First, we need to install the necessary packages. I prefer starting with a clean NestJS project and adding GraphQL support from the beginning.
nest new graphql-api
cd graphql-api
npm install @nestjs/graphql @nestjs/apollo graphql
npm install prisma @prisma/client
npm install redis @nestjs/cache-manager
Why do I choose this specific stack? NestJS provides excellent structure for large applications, Prisma makes database interactions safe and predictable, and Redis handles caching needs efficiently. Together they create a solid foundation.
Setting up the main application module is crucial. Here’s how I configure the GraphQL module with proper error handling:
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
context: ({ req }) => ({ req }),
formatError: (error) => ({
message: error.message,
code: error.extensions?.code,
}),
}),
],
})
export class AppModule {}
Have you ever wondered how to structure your database effectively? Prisma’s schema language makes this intuitive. I design my models with relationships that match real-world connections.
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
author User @relation(fields: [authorId], references: [id])
authorId String
comments Comment[]
}
The Prisma service acts as your gateway to the database. I always extend the base client to include custom methods for complex operations:
@Injectable()
export class PrismaService extends PrismaClient {
async getUserWithPosts(userId: string) {
return this.user.findUnique({
where: { id: userId },
include: { posts: true },
});
}
}
Now, what happens when your API starts getting heavy traffic? This is where Redis caching becomes essential. I integrate it at the service level to automatically cache frequent queries.
@Injectable()
export class UserService {
constructor(
private prisma: PrismaService,
@Inject(CACHE_MANAGER) private cacheManager: Cache
) {}
async findUserById(id: string) {
const cachedUser = await this.cacheManager.get(`user:${id}`);
if (cachedUser) return cachedUser;
const user = await this.prisma.user.findUnique({ where: { id } });
await this.cacheManager.set(`user:${id}`, user, 300);
return user;
}
}
Building resolvers requires careful thought about data fetching. I use the DataLoader pattern to prevent N+1 query problems that can cripple performance.
@Resolver(() => User)
export class UserResolver {
constructor(
private userService: UserService,
private postsService: PostsService
) {}
@Query(() => User)
async user(@Args('id') id: string) {
return this.userService.findUserById(id);
}
@ResolveField(() => [Post])
async posts(@Parent() user: User) {
return this.postsService.findByAuthorId(user.id);
}
}
Security can’t be an afterthought. I implement authentication using JWT tokens and protect sensitive operations with guards.
@Query(() => User)
@UseGuards(GqlAuthGuard)
async currentUser(@Context() context) {
return this.userService.findUserById(context.req.user.id);
}
Error handling deserves special attention. I create custom filters to ensure clients receive consistent, helpful error messages.
@Catch(PrismaClientKnownRequestError)
export class PrismaClientExceptionFilter implements GqlExceptionFilter {
catch(exception: PrismaClientKnownRequestError, host: ArgumentsHost) {
const errorMap = {
P2002: 'Unique constraint failed',
P2025: 'Record not found',
};
throw new HttpException(
errorMap[exception.code] || 'Database error',
HttpStatus.BAD_REQUEST
);
}
}
Testing might seem tedious, but it saves hours of debugging. I write integration tests that verify my resolvers work correctly with the database.
describe('UserResolver', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
it('should get user by id', async () => {
const query = `
query {
user(id: "1") {
id
email
}
}
`;
const result = await request(app.getHttpServer())
.post('/graphql')
.send({ query });
expect(result.body.data.user.id).toBeDefined();
});
});
Performance monitoring helps identify bottlenecks before they become problems. I add simple logging to track query execution times.
@Injectable()
export class LoggingPlugin implements ApolloServerPlugin {
requestDidStart() {
const start = Date.now();
return {
willSendResponse({ response }) {
const duration = Date.now() - start;
console.log(`Query took ${duration}ms`);
},
};
}
}
What separates a good API from a great one? Consistent performance under load, clear error messages, and maintainable code. By combining NestJS’s structure with Prisma’s type safety and Redis’s speed, you create something that scales gracefully.
I’d love to hear about your experiences building GraphQL APIs. What challenges have you faced? Share your thoughts in the comments below, and if you found this helpful, please like and share with other developers who might benefit from these patterns.