This article will teach you how to build a GraphQL API with Next.js to implement the basic CRUD operations using apollo-server-micro, TypeGraphQL, MongoDB, Redis, Mongoose, and Typegoose.

Next.js GraphQL API with TypeGraphQL Series:

  1. GraphQL API with Next.js & MongoDB: Access & Refresh Tokens
  2. GraphQL CRUD API with Next.js, MongoDB, and TypeGraphQL
  3. Next.js, GraphQL-CodeGen, & React Query: JWT Authentication

Related Articles:

GraphQL CRUD API with Next.js, MongoDB, and TypeGraphQL

What is TypeGraphQL?

TypeGraphQL is a Node.js framework that follows the object-oriented approach of defining schemas and resolvers using Typescript classes and decorators.

Prerequisites

Before getting started, ensure you have the following:

  • Node.js and Docker installed on your machine
  • Basic knowledge of JavaScript and TypeScript.
  • Should be familiar with GraphQL APIs.

In order to perform the GraphQL CRUD operations, you must first implement the JWT Authentication since the GraphQL server will require the currently logged-in user’s ID to perform the CRUD operations.

Initialize a Typescript Next.js Project

The first step is to initialize a Next.js Typescript boilerplate application using the Next.js project scaffolding tool (next-app):


yarn create next-app nextjs-typegraphql --typescript
# or
npx create-next-app@latest nextjs-typegraphql --typescript

Next, replace the content of the tsconfig.json file with these configurations to inform the TypeScript compiler of the options to use when transpiling the code to JavaScript.

tsconfig.json


{
  "compilerOptions": {
    "target": "es2018",
    "lib": ["dom", "dom.iterable", "esnext", "ESNext.AsyncIterable"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}


  • emitDecoratorMetadata – Must be set to true to enable experimental support for emitting type metadata for the decorators we will be writing.
  • experimentalDecorators – Must be set to true to enable experimental support for decorators.
  • strictPropertyInitialization – Must be set to false to prevent Typescript from raising errors when we declare the Typegoose class properties without initializing them in the constructor.
  • target – Must be set to es2018 in order for TypeGraphQL to use the features from ES2018.
  • ESNext.AsyncIterable – Must be added to the lib array since thegraphql-subscription dependency depends on an AsyncIterator .

Setup MongoDB and Redis Databases

MongoDB is an open-source NoSQL database that supports rapid iterative development and stores data in BSON (Binary JSON) format.

Redis on the other hand is an open-source, in-memory, key-value pair data store, used as a database, cache, and message broker.

The quickest way to get the Redis and MongoDB databases running on your machine is to use Docker and Docker-compose.

At this point, I assume you already have Docker and Docker-compose installed on your machine.

Now create a docker-compose.yml file in the root directory and add the following configurations to set up the MongoDB and Redis containers.

docker-compose.yml


version: '3.8'
services:
  mongo:
    image: mongo
    container_name: mongodb
    ports:
      - '6000:27017'
    volumes:
      - mongodb:/data/db
    env_file:
      - ./.env
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
      MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE}
  redis:
    image: redis:latest
    container_name: redis
    ports:
      - '6379:6379'
    volumes:
      - redis:/data
volumes:
  redis:
  mongodb:


Next, create a .env file in the root directory and add the following credentials that will be used by the MongoDB Docker image to configure the MongoDB database.

.env


MONGO_INITDB_ROOT_USERNAME=admin
MONGO_INITDB_ROOT_PASSWORD=password123
MONGO_INITDB_DATABASE=nextjs_typegraphql

Start the Redis and MongoDB Docker containers with this command:


docker-compose up -d

Ensure the containers are running with this command:


docker ps

Stop the containers with this command:


docker-compose down

Setting up Environment Variables

Out of the box, Next.js has built-in support for loading environment variables from the .env.local file.

To load the environment variables into the browser, you will have to prefix them with NEXT_PUBLIC_ whereas the variables without the NEXT_PUBLIC_ prefix will be loaded into the Node.js environment allowing you to use them in the Next.js data fetching methods and API routes.

.env.local


MONGODB_LOCAL_URI=mongodb://admin:password123@localhost:6000/nextjs_typegraphql?authSource=admin

Remember to include both the .env and .env.local files in your .gitignore to avoid pushing them to GitHub.

Connecting to the Redis and MongoDB Databases

Now that we have the Redis and MongoDB servers running smoothly, let’s create some utility functions to connect them to the Next.js application.

Connect to the MongoDB Database

Let’s face it, working directly with the MongoDB driver to implement validations and type casting can be challenging. Due to this reason, the Mongoose library was created to make our lives easier.

By default, Mongoose has built-in features like type casting, validation, query builders, pre/post hooks, and more.

Install Mongoose


yarn add mongoose
# or
npm i mongoose

In the root project, create a server/utils/connectDB.ts file and add the following code:

server/utils/connectDB.ts


import mongoose from 'mongoose';

const localUri = process.env.MONGODB_LOCAL_URI as string;
const connection: any = {};
export async function connectDB() {
  if (connection.isConnected) {
    console.log('DB is already connected');
    return;
  }

  if (mongoose.connections.length > 0) {
    connection.isConnected = mongoose.connections[0].readyState;
    if (connection.isConnected === 1) {
      console.log('use previous connection');
      return;
    }
    await mongoose.disconnect();
  }

  const db = await mongoose.connect(localUri);
  console.log('? MongoDB Database Connected Successfully');
  connection.isConnected = db.connections[0].readyState;
}

export async function disconnectDB() {
  if (connection.isConnected) {
    if (process.env.NODE_ENV === 'production') {
      await mongoose.disconnect();
      connection.isConnected = false;
    } else {
      console.log('not discounted');
    }
  }
}

Connect to the Redis Database

Install the Node.js Redis package:


yarn add redis
# or 
npm install redis

To connect to the Redis server, create a server/utils/connectRedis.ts file in the root directory and add the following code:

server/utils/connectRedis.ts


import { createClient } from 'redis';

const redisUrl = 'redis://localhost:6379';

const redisClient = createClient({
  url: redisUrl,
});

const connectRedis = async () => {
  try {
    await redisClient.connect();
  } catch (error: any) {
    setInterval(connectRedis, 5000);
  }
};

connectRedis();

redisClient.on('connect', () =>
  console.log('? Redis client connected successfully')
);

redisClient.on('error', (err) => console.error(err));

export default redisClient;

Setup the GraphQL Apollo Server in Next.js

To configure the Next.js app to run the Apollo server, create a graphql.ts file in the api folder.

Run the following command in the terminal to install the necessary dependencies:


yarn add reflect-metadata apollo-server-micro type-graphql cors micro graphql@15.x
# or 
npm install reflect-metadata apollo-server-micro type-graphql cors micro graphql@15.x

  • apollo-server-micro – is the Micro integration for building production-ready GraphQL APIs.
  • type-graphql – a Node.js framework that uses an object-oriented paradigm to build GraphQL schemas and resolvers using Typescript classes and decorators.
  • cors – configures the Next.js GraphQL API to accept requests from cross-origin domains.

With that out of the way, add the following code to the pages/api/graphql.ts file to enable us to set up and initialize the Apollo GraphQL server.

pages/api/graphql.ts


import 'reflect-metadata';
import { NextApiRequest, NextApiResponse } from 'next';
import { ApolloServer } from 'apollo-server-micro';
import { buildSchema, Query, Resolver } from 'type-graphql';
import Cors from 'cors';
import { connectDB } from '../../server/utils/connectDB';

// Setup cors
const cors = Cors({
  methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
  credentials: true,
  origin: [
    'https://studio.apollographql.com',
    'http://localhost:8000',
    'http://localhost:3000',
  ],
});

// Middleware to run the cors configuration
function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: any) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result: any) => {
      if (result instanceof Error) {
        return reject(result);
      }

      return resolve(result);
    });
  });
}

@Resolver()
class HelloResolver {
  @Query(() => String)
  hello() {
    return 'Hello World!';
  }
}

const schema = await buildSchema({
  resolvers: [HelloResolver],
});

const server = new ApolloServer({
  schema,
});

export const config = {
  api: {
    bodyParser: false,
  },
};

const startServer = server.start();

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  await runMiddleware(req, res, cors);
  await connectDB();
  await startServer;
  await server.createHandler({ path: '/api/graphql' })(req, res);
}

Before we start the server, there is one Webpack configuration we have to add to the next.config.js file to enable top-level await .

next.config.js


/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  webpack: (config) => {
    config.experiments = config.experiments || {};
    config.experiments.topLevelAwait = true;
    return config;
  },
};

module.exports = nextConfig;

Now we’re ready to start the GraphQL server. Start the Next.js development server with the following command:


yarn dev
# or
npm run dev

Open http://localhost:3000/api/graphql in your preferred browser and you should see the Apollo Server default landing page:

nextjs graphql typegraphql api apollo server gui

Click the “Query your server” button to open the Apollo Sandbox GUI and execute the hello query in the operation panel. After the query is successful, you will get the “Hello World!” message.

nextjs graphql typegraphql api make first request

Creating TypeGraphQL and Typegoose Schemas

The GraphQL schema also known as type definitions refers to the data structure of the queries and mutations.

Install the following dependencies:


yarn add @typegoose/typegoose class-validator
# or 
npm install @typegoose/typegoose class-validator

  • @typegoose/typegoose – a library that has tools for writing Mongoose models using Typescript classes and decorators.
  • class-validator – a validation library that supports either decorator or non-decorator-based validation.

Now in the server folder, create a models/post.model.ts file and add the following module imports:

server/models/post.model.ts


import {
  getModelForClass,
  ModelOptions,
  prop,
  Severity,
} from '@typegoose/typegoose';
import type { Ref } from '@typegoose/typegoose';
import { User } from './user.model';

Creating the Typegoose Schema

Typegoose is a Node.js library that allows developers to build Mongoose models using Typescript decorators and classes. The library makes heavy use of Typescript decorators to make the Mongoose models type-rich with Typescript.

server/models/post.model.ts


@ModelOptions({
  schemaOptions: {
    timestamps: true,
  },
  options: {
    allowMixed: Severity.ALLOW,
  },
})
export class Post {
  readonly _id: string;

  @prop({ required: true, unique: true })
  title: string;

  @prop({ required: true })
  content: string;

  @prop({ required: true })
  category: string;

  @prop({ default: 'default.jpeg' })
  image: string;

  @prop({ required: true, ref: () => User })
  user: Ref<User>;
}

const PostModel = getModelForClass<typeof Post>(Post);
export default PostModel;

Creating the TypeGraphQL Schemas

Similar to GraphQL Nexus, TypeGraphQL builds upon the reference implementation for GraphQL but adds built-in TypeScript support making it easier for developers to create type-safe schemas and resolvers.

The schemas are built by defining classes and decorating them and their properties with Typescript decorators.

TypeGraphQL provides us with @InputType() and @ObjectType() decorators that we can use to define the structure of the GraphQL objects and input types.

Create a server/schemas/post.schema.ts file and add the following code:

server/schemas/post.schema.ts


import { MinLength } from 'class-validator';
import { Field, InputType, ObjectType } from 'type-graphql';
import { UserData } from './user.schema';

@InputType()
export class UpdatePostInput {
  @MinLength(10, { message: 'Title must be at least 10 characters long' })
  @Field(() => String, { nullable: true })
  title: string;

  @MinLength(10, { message: 'Content must be at least 10 characters long' })
  @Field(() => String, { nullable: true })
  content: string;

  @Field(() => String, { nullable: true })
  category: string;

  @Field(() => String, { nullable: true })
  image: string;
}

@InputType()
export class PostInput {
  @MinLength(10, { message: 'Title must be at least 10 characters long' })
  @Field(() => String)
  title: string;

  @MinLength(10, { message: 'Content must be at least 10 characters long' })
  @Field(() => String)
  content: string;

  @Field(() => String)
  category: string;

  @Field(() => String)
  image: string;
}

@InputType()
export class PostFilter {
  @Field(() => Number, { nullable: true, defaultValue: 1 })
  page: number;

  @Field(() => Number, { nullable: true, defaultValue: 10 })
  limit: number;
}

@ObjectType()
export class PostDataObj {
  @Field(() => String)
  readonly _id: string;

  @Field(() => String, { nullable: true })
  readonly id: string;

  @Field(() => String)
  title: string;

  @Field(() => String)
  category: string;

  @Field(() => String)
  content: string;

  @Field(() => String)
  image: string;

  @Field(() => Date)
  createdAt: Date;

  @Field(() => Date)
  updatedAt: Date;
}

@ObjectType()
export class PostPopulatedData extends PostDataObj {
  @Field(() => UserData)
  user: UserData;
}

@ObjectType()
export class PostData extends PostDataObj {
  @Field(() => String)
  user: string;
}

@ObjectType()
export class PostResponse {
  @Field(() => String)
  status: string;

  @Field(() => PostData)
  post: PostData;
}

@ObjectType()
export class PostPopulatedResponse {
  @Field(() => String)
  status: string;

  @Field(() => PostPopulatedData)
  post: PostPopulatedData;
}

@ObjectType()
export class PostListResponse {
  @Field(() => String)
  status: string;

  @Field(() => Number)
  results: number;

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

In brief, the classes decorated with @InputType() describes the structure of the incoming GraphQL data whereas the classes decorated with @ObjectType() describes the structure of the outgoing GraphQL data.

In the above, you will notice we excluded the ID field from the input classes because it will be automatically generated by the MongoDB server.

Create a Global Error Handler

Now we are ready to create a global error controller to handle all the possible errors the Next.js application will generate.

Luckily for us, the Apollo server package has built-in error classes that we can use to send appropriate GraphQL errors to the client.

server/controllers/error.controller.ts


import { ValidationError } from 'apollo-server-micro';

const handleCastError = (error: any) => {
  const message = `Invalid ${error.path}: ${error.value}`;
  throw new ValidationError(message);
};

const handleValidationError = (error: any) => {
  const message = Object.values(error.errors).map((el: any) => el.message);
  throw new ValidationError(`Invalid input: ${message.join(', ')}`);
};

const errorHandler = (err: any) => {
  if (err.name === 'CastError') handleCastError(err);
  if (err.name === 'ValidationError') handleValidationError(err);
  throw err;
};

export default errorHandler;

In the above, we handled two possible Mongoose errors to prevent the Next.js application from crashing.

Creating the CRUD Services

Now that we have the Typegoose models, TypeGraphQL schemas, and error handler defined, it’s now time to create the CRUD services.

Before defining the CRUD services, let’s create a custom context type to be used in the CRUD services.

In the server folder, create a types/context.ts file and add the following code:

server/types/context.ts


import { NextApiRequest, NextApiResponse } from 'next';
import { User } from '../models/user.model';

export type Context = {
  req: NextApiRequest;
  res: NextApiResponse;
  deserializeUser: (
    req: NextApiRequest,
    res: NextApiResponse
  ) => Promise<User | undefined>;
};

GraphQL Create Post Service

In order to create a post in the database, you must first be authenticated by the GraphQL server since the user ID field required by the post model will be retrieved from the logged-in user’s information.

The title field of the post model has a unique constraint and MongoDB will raise an error with a 11000 code indicating that a unique constraint on the title field has been violated.

server/services/post.service.ts


import { ValidationError } from 'apollo-server-core';
import errorHandler from '../controllers/error.controller';
import deserializeUser from '../middleware/deserializeUser';
import { PostFilter, PostInput } from '../schemas/post.schema';
import PostModel from '../models/post.model';
import { Context } from '../types/context';

export default class PostService {
  // Create a new post
  async createPost(input: Partial<PostInput>, { req, res }: Context) {
    try {
      const user = await deserializeUser(req, res);
      const post = await PostModel.create({ ...input, user: user?._id });
      return {
        status: 'success',
        post: {
          ...post.toJSON(),
          id: post?._id,
        },
      };
    } catch (error: any) {
      if (error.code === 11000)
        throw new ValidationError('Post with that title already exist');
      errorHandler(error);
    }
  }
}

GraphQL Get a Single Post Service

server/services/post.service.ts


export default class PostService {
  // Create a new post

  // Get a single post
  async getPost(id: string, { req, res }: Context) {
    try {
      await deserializeUser(req, res);
      const post = await PostModel.findById(id).populate('user').lean();

      if (!post) return new ValidationError('No post with that id exists');

      return {
        status: 'success',
        post: {
          ...post,
          id: post?._id,
        },
      };
    } catch (error: any) {
      errorHandler(error);
    }
  }
}

GraphQL Update Post Service

server/services/post.service.ts


export default class PostService {
  // Create a new post

  // Get a single post

  // Update a post
  async updatePost(
    id: string,
    input: Partial<PostInput>,
    { req, res }: Context
  ) {
    try {
      const user = await deserializeUser(req, res);
      const post = await PostModel.findByIdAndUpdate(
        id,
        { ...input, user: user?._id },
        {
          new: true,
          runValidators: true,
          lean: true,
        }
      );

      if (!post) return new ValidationError('No post with that id exists');
      return {
        status: 'success',
        post: {
          ...post,
          id: post?._id,
        },
      };
    } catch (error: any) {
      errorHandler(error);
    }
  }
}

GraphQL Get all Posts Service

server/services/post.service.ts


export default class PostService {
  // Create a new post

  // Get a single post

  // Update a post

  // Get all posts
  async getPosts(input: PostFilter, { req, res }: Context) {
    try {
      const user = await deserializeUser(req, res);
      const postsQuery = PostModel.find({ user: user?._id }).populate('user');

      // Pagination
      const page = input.page || 1;
      const limit = input.limit || 10;
      const skip = (page - 1) * limit;

      const posts = await postsQuery
        .sort({ createdAt: -1 })
        .skip(skip)
        .limit(limit)
        .lean();
      return {
        status: 'success',
        results: posts.length,
        posts,
      };
    } catch (error: any) {
      errorHandler(error);
    }
  }
}

GraphQL Delete Post Service

server/services/post.service.ts


export default class PostService {
  // Create a new post

  // Get a single post

  // Update a post

  // Get all posts

  // Delete a post
  async deletePost(id: string, { req, res }: Context) {
    try {
      await deserializeUser(req, res);
      const post = await PostModel.findByIdAndDelete(id);

      if (!post) return new ValidationError('No post with that id exists');

      return true;
    } catch (error: any) {
      errorHandler(error);
    }
  }
}

Complete GraphQL CRUD Services Code

server/services/post.service.ts


export default class PostService {
  // Create a new post
  async createPost(input: Partial<PostInput>, { req, res }: Context) {
    try {
      const user = await deserializeUser(req, res);
      const post = await PostModel.create({ ...input, user: user?._id });
      return {
        status: 'success',
        post: {
          ...post.toJSON(),
          id: post?._id,
        },
      };
    } catch (error: any) {
      if (error.code === 11000)
        throw new ValidationError('Post with that title already exist');
      errorHandler(error);
    }
  }

  // Get a single post
  async getPost(id: string, { req, res }: Context) {
    try {
      await deserializeUser(req, res);
      const post = await PostModel.findById(id).populate('user').lean();

      if (!post) return new ValidationError('No post with that id exists');

      return {
        status: 'success',
        post: {
          ...post,
          id: post?._id,
        },
      };
    } catch (error: any) {
      errorHandler(error);
    }
  }

  // Update a post
  async updatePost(
    id: string,
    input: Partial<PostInput>,
    { req, res }: Context
  ) {
    try {
      const user = await deserializeUser(req, res);
      const post = await PostModel.findByIdAndUpdate(
        id,
        { ...input, user: user?._id },
        {
          new: true,
          runValidators: true,
          lean: true,
        }
      );

      if (!post) return new ValidationError('No post with that id exists');
      return {
        status: 'success',
        post: {
          ...post,
          id: post?._id,
        },
      };
    } catch (error: any) {
      errorHandler(error);
    }
  }

  // Get all posts
  async getPosts(input: PostFilter, { req, res }: Context) {
    try {
      const user = await deserializeUser(req, res);
      const postsQuery = PostModel.find({ user: user?._id }).populate('user');

      // Pagination
      const page = input.page || 1;
      const limit = input.limit || 10;
      const skip = (page - 1) * limit;

      const posts = await postsQuery
        .sort({ createdAt: -1 })
        .skip(skip)
        .limit(limit)
        .lean();
      return {
        status: 'success',
        results: posts.length,
        posts,
      };
    } catch (error: any) {
      errorHandler(error);
    }
  }

  // Delete a post
  async deletePost(id: string, { req, res }: Context) {
    try {
      await deserializeUser(req, res);
      const post = await PostModel.findByIdAndDelete(id);

      if (!post) return new ValidationError('No post with that id exists');

      return true;
    } catch (error: any) {
      errorHandler(error);
    }
  }
}

Creating the CRUD TypeGraphQL Resolvers

Now we are ready to create the queries and mutations with TypeGraphQL to implement the CRUD operations. To create queries and mutations with TypeGraphQL, we need to create a class and decorate the class and its methods with the TypeGraphQL decorators.

server/resolvers/post.resolver.ts


import { Arg, Args, Ctx, Mutation, Query, Resolver } from 'type-graphql';
import {
  PostFilter,
  PostInput,
  PostListResponse,
  PostPopulatedResponse,
  PostResponse,
  UpdatePostInput,
} from '../schemas/post.schema';
import PostService from '../services/post.service';
import type { Context } from '../types/context';

@Resolver()
export default class PostResolver {
  constructor(private postService: PostService) {
    this.postService = new PostService();
  }

  @Mutation(() => PostResponse)
  async createPost(@Arg('input') input: PostInput, @Ctx() ctx: Context) {
    return this.postService.createPost(input, ctx);
  }

  @Query(() => PostPopulatedResponse)
  async getPost(@Arg('id') id: string, @Ctx() ctx: Context) {
    return this.postService.getPost(id, ctx);
  }

  @Mutation(() => PostResponse)
  async updatePost(
    @Arg('id') id: string,
    @Arg('input') input: UpdatePostInput,
    @Ctx() ctx: Context
  ) {
    return this.postService.updatePost(id, input, ctx);
  }

  @Query(() => PostListResponse)
  async getPosts(
    @Arg('input', { nullable: true }) input: PostFilter,
    @Ctx() ctx: Context
  ) {
    return this.postService.getPosts(input, ctx);
  }

  @Mutation(() => Boolean)
  async deletePost(@Arg('id') id: string, @Ctx() ctx: Context) {
    return this.postService.deletePost(id, ctx);
  }
}

In the above, you will notice we leveraged Dependency Injection to decouple the business logic from the services since TypeGraphQL was built with an object-oriented paradigm in mind.

server/resolvers/index.ts


import UserResolver from './user.resolver';
import PostResolver from './post.resolver';

export const resolvers = [UserResolver, PostResolver] as const;

Update the Apollo GraphQL Server

Now let’s bring everything together in the ./pages/api/graphql.ts file. Import the resolver exported in server/resolvers/index.ts file and add it to the schema builder to register the CRUD services.

pages/api/graphql.ts


import 'reflect-metadata';
import { NextApiRequest, NextApiResponse } from 'next';
import { ApolloServer } from 'apollo-server-micro';
import { buildSchema } from 'type-graphql';
import Cors from 'cors';
import { resolvers } from '../../server/resolvers';
import { connectDB } from '../../server/utils/connectDB';
import deserializeUser from '../../server/middleware/deserializeUser';

const cors = Cors({
  methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
  credentials: true,
  origin: [
    'https://studio.apollographql.com',
    'http://localhost:8000',
    'http://localhost:3000',
  ],
});

function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: any) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result: any) => {
      if (result instanceof Error) {
        return reject(result);
      }

      return resolve(result);
    });
  });
}

const schema = await buildSchema({
  resolvers,
  dateScalarMode: 'isoDate',
});

const server = new ApolloServer({
  schema,
  csrfPrevention: true,
  context: ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) => ({
    req,
    res,
    deserializeUser,
  }),
});

export const config = {
  api: {
    bodyParser: false,
  },
};

const startServer = server.start();

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  await runMiddleware(req, res, cors);
  await connectDB();
  await startServer;
  await server.createHandler({ path: '/api/graphql' })(req, res);
}

Start the MongoDB and Redis containers:


docker-compose up -d

Start the Next.js development server:


yarn dev
# or 
npm run dev

Testing the GraphQL CRUD API in Postman

I switched from testing the GraphQL API in the Apollo Sandbox to Postman because there was some issue with the Sandbox not setting the cookies.

You can download and import the collection into your Postman to avoid typing all the queries and mutations from scratch.

-GraphQL Create Post Mutation

nextjs graphql crud api create post

-GraphQL Update Post Mutation

nextjs graphql crud api update post

-GraphQL Get a Single Post Query

nextjs graphql crud api get a single post

-GraphQL Get All Posts Query

nextjs graphql crud api retrieve all posts

-GraphQL Delete Post Mutation

nextjs graphql crud api delete post

Conclusion

With this Next.js and Typescript GraphQL CRUD API example with TypeGraphQL, apollo-server-micro, MongoDB, Class Validator, Typegoose, and Mongoose, you’ve learned how to build a GraphQL API with Next.js to implement the basic CRUD (Create/Read/Update/Delete) operations.

Next.js GraphQL CRUD API Source Code

You can find the complete source code on my GitHub page