Lately, I’ve been thinking about how modern applications demand both speed and flexibility from their APIs. When clients ask for real-time features and complex data relationships, traditional REST approaches often fall short. That’s why I decided to explore a full stack solution combining NestJS, Prisma, and Redis. The result? A GraphQL API that handles demanding production workloads while keeping code maintainable.
Getting started requires a solid foundation. First, I set up a new NestJS project and installed key dependencies. The structure organizes functionality into discrete modules - users, posts, comments - with shared utilities like authentication and caching in common directories. This separation keeps concerns isolated as the project grows.
nest new graphql-api-tutorial
npm install @nestjs/graphql @prisma/client redis @nestjs/cache-manager
For the database layer, Prisma’s declarative schema shines. I modeled users, posts, comments, and tags with relations directly in the schema file. Notice how the @@unique
constraint prevents duplicate likes? These small details prevent data inconsistencies. The PrismaService extends the client with custom methods like findPostsWithStats()
, which fetches post data with aggregated counts in a single query.
model Like {
id String @id @default(cuid())
userId String
postId String
@@unique([userId, postId]) // Prevent duplicate likes
}
Configuring the GraphQL module was straightforward. I enabled both WebSocket protocols for subscriptions and added error formatting for consistent client responses. The autoSchemaFile option generates SDL from decorators - a huge time-saver.
GraphQLModule.forRoot({
driver: ApolloDriver,
autoSchemaFile: true,
subscriptions: {
'graphql-ws': true,
'subscriptions-transport-ws': true
}
})
When building resolvers, I leveraged NestJS’s decorator system. For example, the @ResolveField()
decorator efficiently loads author data for posts. But what happens when a popular post gets thousands of requests? Without caching, databases buckle under load.
@ResolveField('author', () => User)
async getAuthor(@Parent() post: Post) {
return this.prisma.user.findUnique({
where: { id: post.authorId }
});
}
Redis integration solved this. The CacheModule connects to Redis with environment-based configuration. In resolver methods, I check the cache before querying the database. When data changes, I invalidate related keys. Notice the 5-minute TTL balances freshness with performance.
const cachedPosts = await this.cache.get(`user:${userId}:posts`);
if (cachedPosts) return cachedPosts;
const posts = await this.postService.findByUser(userId);
await this.cache.set(`user:${userId}:posts`, posts, 300);
For authentication, I used Passport with JWT strategy. The @UseGuards(GqlAuthGuard)
decorator protects resolvers, while custom decorators like @CurrentUser()
inject the authenticated user. Authorization rules live in service methods - keeping resolvers lean.
Real-time subscriptions required careful planning. Using @Subscription()
decorators with GraphQL PubSub, I implemented comment notifications. But how do we scale this beyond a single instance? In production, I’d switch to Redis PubSub for distributed systems.
The N+1 query problem emerged when loading nested relations. DataLoader batches requests automatically. Creating a loader per request context is crucial - shared loaders cause stale data.
@Injectable({ scope: Scope.REQUEST })
export class UserLoaders {
constructor(private prisma: PrismaService) {}
createBatchUsers() {
return new DataLoader<string, User>(async (userIds) => {
const users = await this.prisma.user.findMany({
where: { id: { in: [...userIds] } }
});
return userIds.map(id => users.find(u => u.id === id));
});
}
}
Testing involved both unit tests for services and integration tests for GraphQL queries. For deployment, I used Docker with multi-stage builds. Monitoring with Apollo Studio provided query performance insights.
This stack delivers remarkable results. In load tests, cached responses handled 15x more requests per second compared to uncached endpoints. The type safety from Prisma and NestJS caught errors during development, not production.
If you’ve struggled with API performance or complex data graphs, try this combination. The developer experience is superb, and the results speak for themselves. Found this approach helpful? Share your thoughts in the comments or pass this along to a colleague facing similar challenges.