js

Build Production-Ready GraphQL API with NestJS, Prisma and Redis Caching - Complete Tutorial

Learn to build scalable GraphQL APIs with NestJS, Prisma, and Redis caching. Master authentication, real-time subscriptions, and production deployment.

Build Production-Ready GraphQL API with NestJS, Prisma and Redis Caching - Complete Tutorial

I’ve been thinking about modern API development recently. The shift toward GraphQL offers real benefits, but building production-ready systems requires careful choices. That’s why I want to share a robust approach combining NestJS, Prisma, and Redis. These tools create efficient, maintainable APIs that scale. Let’s explore how they work together.

Starting a new project? Here’s how I set up the foundation:

nest new graphql-api-tutorial
npm install @nestjs/graphql prisma @prisma/client redis @nestjs/redis

My project structure organizes functionality:

src/
├── auth/
├── common/
├── database/
├── modules/
└── redis/

Core configuration connects everything:

@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql',
      subscriptions: { 'graphql-ws': true }
    }),
    PrismaModule,
    RedisModule
  ]
})
export class AppModule {}

Database design comes next. Prisma’s schema language clearly defines models:

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  posts     Post[]
}

model Post {
  id        String   @id @default(cuid()
  title     String
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
}

The Prisma service handles connections efficiently:

@Injectable()
export class PrismaService extends PrismaClient {
  async onModuleInit() {
    await this.$connect();
    this.$use(async (params, next) => {
      const start = Date.now();
      const result = await next(params);
      console.log(`Query took ${Date.now() - start}ms`);
      return result;
    });
  }
}

Defining GraphQL types ensures clean contracts. Notice how relationships map:

@ObjectType()
export class User {
  @Field(() => ID)
  id: string;

  @Field(() => [Post])
  posts: Post[];
}

Input validation keeps data clean:

@InputType()
export class CreatePostInput {
  @Field()
  @IsString()
  @MinLength(5)
  title: string;
}

Resolvers become the bridge between schema and data. How do we make them efficient?

@Resolver(() => User)
export class UserResolver {
  constructor(private prisma: PrismaService) {}

  @Query(() => User)
  async user(@Args('id') id: string) {
    return this.prisma.user.findUnique({ where: { id } });
  }
}

Caching frequently accessed data boosts performance significantly. Redis integration is straightforward:

@Injectable()
export class RedisService {
  constructor(@InjectRedis() private readonly redis: Redis) {}

  async get(key: string): Promise<string> {
    return this.redis.get(key);
  }

  async set(key: string, value: string, ttl: number) {
    await this.redis.set(key, value, 'EX', ttl);
  }
}

Applying caching in resolvers prevents database overload:

async user(@Args('id') id: string) {
  const cached = await this.redisService.get(`user:${id}`);
  if (cached) return JSON.parse(cached);
  
  const user = await this.prisma.user.findUnique({ where: { id } });
  await this.redisService.set(`user:${id}`, JSON.stringify(user), 300);
  return user;
}

Authentication secures our API. JWT strategies integrate cleanly:

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

Real-time subscriptions add powerful capabilities. Setup is surprisingly simple:

@Subscription(() => Post, {
  resolve: (payload) => payload.newPost
})
newPost() {
  return pubSub.asyncIterator('NEW_POST');
}

Performance optimization matters at scale. The N+1 problem? DataLoader solves it elegantly:

@Injectable()
export class PostsLoader {
  constructor(private prisma: PrismaService) {}

  createLoader() {
    return new DataLoader<string, Post[]>(async (userIds) => {
      const posts = await this.prisma.post.findMany({
        where: { authorId: { in: [...userIds] } }
      });
      return userIds.map(id => posts.filter(p => p.authorId === id));
    });
  }
}

Testing ensures reliability. I focus on key areas:

describe('UserResolver', () => {
  let resolver: UserResolver;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [UserResolver, PrismaService]
    }).compile();

    resolver = module.get<UserResolver>(UserResolver);
  });

  it('returns user by id', async () => {
    const user = await resolver.user('user1');
    expect(user.email).toEqual('test@example.com');
  });
});

Deployment requires attention to environment specifics. My Dockerfile typically includes:

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main.js"]

Monitoring in production catches issues early. I combine logging with tools like Prometheus:

const counter = new Prom.Counter({
  name: 'graphql_requests_total',
  help: 'Total GraphQL requests'
});

app.use((req, res, next) => {
  if (req.path === '/graphql') counter.inc();
  next();
});

This stack delivers production-ready GraphQL APIs efficiently. Each tool solves specific challenges while working harmoniously together. What optimizations have you found most effective in your projects?

If this guide helped clarify GraphQL API construction, consider sharing it with others facing similar challenges. Your experiences and insights matter - what deployment strategies have worked best for you? Leave your thoughts below.

Keywords: NestJS GraphQL API, Prisma ORM PostgreSQL, Redis caching optimization, JWT authentication GraphQL, GraphQL subscriptions real-time, DataLoader N+1 prevention, production GraphQL deployment, scalable NestJS architecture, GraphQL performance optimization, GraphQL API testing strategies



Similar Posts
Blog Image
Build Serverless GraphQL APIs with Apollo Server TypeScript and AWS Lambda Complete Guide

Learn to build scalable serverless GraphQL APIs with Apollo Server, TypeScript & AWS Lambda. Complete guide with authentication, optimization & deployment strategies.

Blog Image
Build Complete Event-Driven Architecture with RabbitMQ TypeScript Microservices Tutorial

Learn to build scalable event-driven microservices with RabbitMQ & TypeScript. Master event sourcing, CQRS, error handling & production deployment.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack web applications. Build faster with auto-generated types and seamless database operations.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Database Operations

Learn to integrate Next.js with Prisma ORM for type-safe, full-stack web applications. Build database-driven apps with seamless frontend-backend unity.

Blog Image
Building Event-Driven Microservices with Node.js, EventStore and gRPC: Complete Architecture Guide

Learn to build scalable distributed systems with Node.js, EventStore & gRPC microservices. Master event sourcing, CQRS patterns & resilient architectures.

Blog Image
Build High-Performance GraphQL APIs: TypeScript, Apollo Server, and DataLoader Pattern Guide

Learn to build high-performance GraphQL APIs with TypeScript, Apollo Server & DataLoader. Solve N+1 queries, optimize database performance & implement caching strategies.