Today's blog is about a graphql. what is graphql? why it is so popular? And How to use Next.js.
Let's understand what is graphql.
Graphql is an open-source alternative to REST APIs. Graphql has been created and maintained by Facebook since 2012.
GraphQL is a query language for APIs and a runtime for executing those queries by using a type system you define for your data. It provides a more efficient, powerful, and flexible alternative to REST.
What is the exact problem how does graphql solve it?
Imagine you are working on a recipe-sharing project and you want to display an image, title, and author name on the recipe card. Traditionally, when you hit the API request to get the data, it might fetch all the data related to the recipe, including unused fields like detailed instructions, ingredients, and timestamps. This leads to over-fetching, where more data than necessary is retrieved, making the application slower and less efficient.
Here, GraphQL comes into the picture and solves this problem. With GraphQL, you can specify exactly which fields you want to retrieve in your query. This gives you the freedom to fetch only the required data, improving efficiency and performance.
Let's see how graphql solves this problem and what is the syntax of graphql Before that let's understand some of the key concepts of graphql.
Here is the list of common concepts in Graphql
Schema: Defines the structure of your data and the queries/mutations you can perform.
Query: Used to fetch data.
Mutation: Used to modify data
Resolver: Functions that resolve a value for a type or field in the schema.
Types: Define the shape of data (e.g.,
Recipe
,Author
).
To understand the graphql concepts better I created a project Recipe Haven ๐ฒ
Where you can list the recipe you want and you can read, update, create, and delete the recipe. you can perform the total Recipe Haven ๐ฒ operations.
Here is the project structure,
So, here is the schema I defined for this project
import { pgTable, serial, text, timestamp, integer, date, pgTableCreator, pgEnum } from "drizzle-orm/pg-core";
import { InferInsertModel } from "drizzle-orm"
export const createTable = pgTableCreator(
(name) => `nomad_competition_${name}`,
);
export const MealTypeEnum = pgEnum('meal_type', ['breakfast', 'lunch', 'dinner', 'dessert']);
export const recipeTable = pgTable("recipes", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
description: text("description").notNull(),
instructions: text("instructions").notNull(),
createdAt: date("created_at").notNull().defaultNow(),
category: text("category")
});
export const categoriesTable = pgTable("categories", {
id: serial("id").primaryKey(),
type: MealTypeEnum("type").notNull()
});
export const recipeCategoriesTable = pgTable("recipe_categories", {
id: serial("id").primaryKey(),
recipeId: integer("recipe_id").references(() => recipeTable.id),
categoryId: integer("category_id").references(() => categoriesTable.id),
});
export type RecipeTable = InferInsertModel<typeof recipeTable>;
export type CategoriesTable = InferInsertModel<typeof categoriesTable>;
export type RecipeCategoriesTable = InferInsertModel<typeof recipeCategoriesTable>;
So I created three tables for this project.
I created a different folder to store all the files related a graphql
Let's go through each file and understand the code.
First, let's see the code to set up a graphql server with an Apollo server. Inside the src/app
a folder, I created another folder graphql
you can name whatever you want. inside the graphql folder create a file name route.ts
.
import { ApolloServer, } from "@apollo/server";
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { typeDefs } from "../../../../graphql/typeDef";
import { resolvers } from "../../../../graphql/resolvers";
const server = new ApolloServer({ typeDefs, resolvers });
const handler = startServerAndCreateNextHandler(server);
export { handler as GET, handler as POST };
In this folder, I imported both the typeDefs & resolvers and set up an ApolloServer. you can check out the code above.
What is typeDefs in Graphql?
In GraphQL, typeDefs
(short for type definitions) are a crucial part of defining your GraphQL schema. They describe the types of data your GraphQL API can return, the structure of those types, and the available queries and mutations.
Here are important components of TypeDefs
Types: Define the shape of your data.
Queries: Define the read operations.
Mutations: Define the write operations.
Here are the typeDefs defined for our project
import { gql } from "@apollo/client"
export const typeDefs = gql`
type Recipe {
id: ID!
name: String
instructions: String
description: String
createdAt: String
category: String
}
type Query {
getAllRecipes: [Recipe!]
recipeByName(name: String!): [Recipe!]
recipesByCategory(category: String!): [Recipe!]
}
type Mutation {
createRecipe(id: ID, name: String, instructions: String, description: String, createdAt: String, category: String): Recipe
updateRecipe(id: ID, name: String, instructions: String, description: String, createdAt: String, category: String): Recipe
deleteRecipe(id: ID!): Recipe
}
`;
gql
is used for the syntax highlighting inside the typeDefs. So let me explain what I write for this project.
Types:
Recipe
: Represents a recipe with various fields likeid
,title
,image
,author
,instructions
,ingredients
, andcreatedAt
.
Queries:
recipeByName(name: String!)
: Fetches a single recipe by its name.getAllRecipes
: Fetches all recipes.recipesByCategory
: Get recipes by category name.
Mutations:
createRecipe
: Creates a new recipe with the specified fields.updateRecipe
: Updates an existing recipe identified byid
.deleteRecipe
: Deletes a recipe byid
and returns a boolean indicating success.
Mutations are more or less Database functions that perform for the different operations.
Here is the whole code for the mutations :
import { gql } from "@apollo/client"
import { db } from "../lib/db/dbconnect"
import { recipeTable } from "../lib/db/schema"
import { eq } from "drizzle-orm";
export const mutation = {
createRecipe: async (parent: any, args: any) => {
const { id, name, instructions, description, createdAt, category } = args;
if (!instructions) {
throw new Error("Instructions are required.");
}
const newRecipe = await db.insert(recipeTable).values({
id,
name,
instructions,
description,
createdAt,
category,
}).returning();
return newRecipe[0];
},
updateRecipe: async (parent: any, args: any) => {
const { id, name, instructions, description, createdAt, category } = args;
console.log('Updating recipe with ID:', id);
console.log('New values:', { name, instructions, description, createdAt, category });
const updatedRecipe = await db.update(recipeTable)
.set({
name,
instructions,
description,
createdAt,
category
})
.where(eq(recipeTable.id, id)).returning();
if (updatedRecipe.length === 0) {
throw new Error("Recipe not found or no changes made.");
}
return updatedRecipe[0];
},
deleteRecipe: async (parent: any, args: any) => {
const { id } = args;
const deletedRecipe = await db.delete(recipeTable)
.where(eq(recipeTable.id, id))
.returning();
if (deletedRecipe.length === 0) {
throw new Error("Recipe not found or could not be deleted.");
}
return deletedRecipe[0];
},
}
For this project, I use the Postgres database so the syntax is like that.
Here is a detailed description of what this mutation does:
createRecipe:
Inserts a new recipe into the database with the provided details.
Returns the newly created recipe.
updateRecipe:
Updates an existing recipe in the database based on the given
id
and new details.Returns the updated recipe or throws an error if no changes were made.
deleteRecipe:
Deletes a recipe from the database identified by the given
id
.Returns the deleted recipe or throws an error if the recipe was not found.
Then let's look at the queries that
import { eq } from "drizzle-orm";
import { db } from "../lib/db/dbconnect";
import { recipeTable } from "../lib/db/schema";
export const queries = {
getAllRecipes: async () => db.select().from(recipeTable).execute(),
recipeByName: async (parent: any, args: any) => {
const { name } = args;
return db.select().from(recipeTable).where(eq(recipeTable.name, name)).execute();
},
recipesByCategory: async (parent: any, args: any) => {
const { category } = args;
console.log(category);
return db.select().from(recipeTable).where(eq(recipeTable.category, category)).execute();
}
}
Here are concise descriptions for each query:
getAllRecipes:
Fetches all recipes from the database.
Returns an array of recipe objects.
recipeByName:
Fetches recipes from the database that match the provided name.
Returns an array of matching recipe objects.
recipesByCategory:
Fetches recipes from the database that belong to the specified category.
Returns an array of matching recipe objects.
let's look at the resolvers,
import { mutation } from "./mutation";
import { queries } from "./quries";
export const resolvers = {
Query: {
...queries,
},
Mutation: {
...mutation,
}
};
For a good project structure, I defined Query
& Mutation
in different files.
Conclusion
In this tutorial, we've explored how GraphQL can be a powerful tool for optimizing your API interactions by allowing clients to specify exactly what data they need. Using our recipe-sharing project as an example, we saw how to define typeDefs
for our schema, implement resolvers for queries and mutations, and handle complex relationships like recipes and their categories.
GraphQL's flexibility and efficiency make it an excellent choice for modern web applications, especially when working with data-intensive projects. By leveraging GraphQL, you can build APIs that are both robust and performant, providing a better experience for both developers and users.
Happy coding! ๐