Creating a Next.js 13 FullStack Application with GraphQL, TypeScript, Prisma, and Vercel — Part 2
Building a simple GraphQL server
An API route in Next.js provides a way to build an API directly into a Next.js app. It provides a solution to serverless functions in JavaScript. The API Routes feature is like an express.js-like solution enabling you to build an API endpoint within a Next.js app. You can use the /api directory to create these routes.
Here is how you can set up a simple GraphQL server in Next.js 13:
- Install the necessary dependencies into your project :
npm install @as-integrations/next @apollo/server graphql
npm install --save-dev apollo @types/graphql
- Create GraphQL Server: Firstly, import needed libraries and define your GraphQL schema and resolvers in ‘
/pages/api/route.ts
', like so:
import { ApolloServer } from "@apollo/server";
import { NextRequest } from "next/server";
import { startServerAndCreateNextHandler } from "@as-integrations/next";
const typeDefs = `#graphql
type Query {
Hello: String
}
`;
const resolvers = {
Query: {
Hello: () => "Word!",
},
};
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
});
const handler = startServerAndCreateNextHandler<NextRequest>(apolloServer);
export async function GET(request: NextRequest) {
return handler(request);
}
export async function POST(request: NextRequest) {
return handler(request);
}
In this case, Hello
is a Query type that returns a string. This Resolver function corresponds to this Query.
- Now you’re ready to test your API route! Start your server with
npm run dev
, and navigate tohttp://localhost:3000/api/graphql
. You should see the Apollo Server playground. You can then query your API like so:
query ExampleQuery {
Hello
}
You should see “Word!” return as a response.
You can find the corresponding source code with tag “Part2.1” here.
Introduction to Prisma and it’s role in data management
Prisma is a powerful and versatile open-source database toolkit that simplifies the process of working with databases. It acts as an interface between your application and the database, providing a type-safe query builder and an auto-generated, type-safe data access layer.
Prisma offers an abstraction layer that allows you to write database queries using a type-safe API. It supports various databases such as PostgreSQL, MySQL, SQLite, and SQL Server, providing you with the flexibility to choose the database that best suits your application’s needs.
One of the significant advantages of using Prisma is that it seamlessly integrates with GraphQL. Prisma’s GraphQL API automates the process of fetching and updating data from the database through GraphQL queries and mutations. It eliminates the need to write complex SQL queries manually, reducing development time and potential errors.
Here are some key benefits and features of Prisma:
- Type Safety: Prisma generates TypeScript or JavaScript code based on your database schema, providing type-safe queries and eliminating runtime errors due to invalid queries.
- Database Migrations: Prisma allows you to manage database schema changes in a structured and organized
Integrating Prisma service with GraphQL
Prerequisites
To start, we have to install Prisma and configure it. For this tutorial, my database is a cockroachdb
. If you use another provider, you can change it (list of providers).
npm install prisma --save-dev
npx prisma init --datasource-provider cockroachdb
Now, we have to configure our database connection. For that, prisma has created a .env
file and DATABASE_URL
environment variable. Here, this variable will be available only in server side. To create environment variable for client side, variables should be prefixe with NEXT_PUBLIC_[variable-name]
(Next.JS documentation). We can change this DATABASE_URL
to use your own url. For me it will be :postgresql://tutorial_user:NOnalIFyPUILKSZzh×6uAg@poodle-chimera-11062.8nj.cockroachlabs.cloud:26257/defaultdb
Configure our database schema
First, let’s define our Prisma data model in prisma/schema.prisma
. In this case, we're gonna make a simple Todo model:
model Todo {
id Int @id @default(sequence())
title String
done Boolean @default(false)
createdAt DateTime @default(now())
}
Your file will be :
To send this new schema in our database and update it, we have to launch npx prisma migrate dev --name init
command. The response should be :
Now Prisma Client can be imported in your application. We will create a new folder named lib
into src/app
. The lib
folder in a Next.JS 13 application is a common convention among developers as a place to store utilities, shared functions, and any form of reusable logic that isn't a component.
In this folder, we create an subfolder named prisma
and a index.ts
file with this content :
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient({
log: ["warn", "error", "query"],
});
export default prisma;
This will allow us to use prisma to query our database.
Integrating Prisma service with GraphQL
In GraphQL, queries and mutations are two different types of operations. Queries are used to fetch or retrieve data, they are read-only operations. They don’t change or manipulate the data on the server.
On the other hand, mutations are write operations. They are used to create, update, or delete data on the server. In essence, queries are akin to the GET method in REST, while mutations resemble POST, PUT, and DELETE methods.
TypeDefs
When using GraphQL in your backend, creating a schema to define your data types, queries, mutations, is essential. Definition of these types is done through TypeDefs (type definitions).
In our ‘Todo’ application, we will create our typeDefs.js
file in which we'll define a Todo Type and the corresponding queries and mutations.
const typeDefs = `#graphql
type Todo {
id: Int!
title: String!
done: Boolean!
createdAt: String!
}
type Query {
todos: [Todo!]!
}
type Mutation {
createTodo(title: String!): Todo!
updateTodo(id: Int!, done: Boolean!): Todo!
}
`;
In the above code:
- We define a ‘Todo’ type, which includes fields of different types including ‘Int!’, ‘String!’, and ‘Boolean!’. The ‘!’ signifies that the field is non-nullable, meaning it cannot be null.
- Next, we create a ‘Query’ type which acts as a read-only fetch operation and defines ways we can fetch data from our API. We define two queries: one to retrieve all todos (
todos
). - Lastly, we create a ‘Mutation’ type that describes operations that can change data on the server. We add three mutations: one for creating a new todo (
createTodo
), and one for updating an existing todo (updateTodo
).
Resolvers
In GraphQL, a resolver is a function that retrieves data for a particular field in the schema from various data sources such as databases, APIs, and even other services. It is responsible for shaping and transforming the data in the response, returning a value that adheres to the type definitions and requirements specified in the GraphQL schema.
The createTodo
resolver is a function that provides the instructions for turning a GraphQL operation (in this case, a mutation) into the data result. In our Todo application, it's used to instruct GraphQL on how to create a new todo record. Let's break down the createTodo
resolver:
const resolvers = {
Mutation: {
createTodo: async (parent: never, args: { title: string }) => {
const newTodo = prisma.todo.create({
data: {
title: args.title,
done: false,
},
});
return newTodo;
},
}
}
In the above code, createTodo
is an asynchronous function that takes in three arguments: parent
, args
, and context
.
parent
is an argument that is rarely used, but it contains the result of the parent field. SincecreateTodo
is a root field, theparent
argument doesn't apply in this case.args
is an object that holds all GraphQL arguments provided in the mutation. We're specifically usingargs.title
to get access to the title submitted in the mutation.
The create
method is called on prisma.todo
to create a new todo record in the database. The data
object represents the values of the record, which include a title
that is received from the mutation argument, and done
flag which is set as false by default.
Lastly, the function returns the newly created todo record. The returned object should conform to the shape of the Todo
type.
import prisma from "@/app/lib/prisma";
const resolvers = {
Mutation: {
createTodo: async (_: never, args: { title: string }) => {
const newTodo = prisma.todo.create({
data: {
title: args.title,
done: false,
},
});
return newTodo;
},
},
}
Lastly, the function returns the newly created todo record. The returned object should conform to the shape of the Todo
type.
The updateTodo
mutation is responsible for updating a specific existing todo record in our application. Take a look at the function for this resolver:
const resolvers = {
Mutation: {
...
updateTodo: async (_: never, args: { done: boolean; id: number }) => {
const updatedTodo = await prisma.todo.update({
where: {
id: args.id,
},
data: {
done: args.done,
},
});
return updatedTodo;
},
},
}
In this mutation, we take in two arguments from args
: id
and done
.
args.id
is used in thewhere
clause to specify which todo record should be updated.args.done
contains our updated value for thedone
field.
Using Prisma's update
method, we can easily specify which todo to update using the where
clause, and what data to update using the data
clause. We then return the updated todo, adhering to the Todo
type structure.
The todos
query is utilized to fetch all existing todos from our database. Here's how the resolver function for this query looks:
const resolvers = {
...
Query: {
todos: async () => prisma.todo.findMany(),
},
}
prisma.todo.findMany()
is the Prisma Client query used to fetch all todos.findMany
is a method provided by Prisma Client that lets you read records from your database.- The function then returns the array of todos. As specified in our type definitions,
todos
query should return an array ofTodo
.
This resolver is mapped to the todos
field of our root Query
type and executed whenever a todos
query is made to our GraphQL API.
Now, you can restart your server and navigate to apollo (http://localhost:3000/api/graphql) where we can create a new todo :
You can test ours two others queries/mutations to check if our create mutate is correctly setup :
You can find the corresponding source code with tag “Part2.2” here.