Creating a simple yet type-safe recipes GraphQL API with Prisma

Victor Iris
7 min readJan 4, 2021

Overview:

  1. Set project structure and dependencies
  2. Set graphql schema
  3. Setup graphql server
  4. Run and enjoy!

note: I am using node (v14.15.3)

To begin with, create your project folder and add the following code in /package.json

{
"name": "recipe-graphql-api",
"version": "0.0.1",
"scripts": {
},
"dependencies": {
"@prisma/client": "^2.13.1",
"dotenv": "^8.2.0",
"graphql-yoga": "^1.18.3",
"nexus": "^1.0.0",
"nexus-plugin-prisma": "^0.27.0"
},
"devDependencies": {
"@prisma/cli": "^2.13.1",
"@types/node": "12.19.11",
"dotenv-cli": "^4.0.0",
"husky": "^4.3.6",
"lint-staged": "^10.5.3",
"prettier": "^2.2.1",
"ts-node": "^9.1.1",
"ts-node-dev": "^1.1.1",
"typescript": "^4.1.3"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --write"
]
},
"prettier": {
"trailingComma": "all",
"tabWidth": 4,
"useTabs": true,
"semi": true,
"singleQuote": false
}
}

Now, install the dependencies with yarn or npm and let’swalk thru the dependencies while that happens. For our API we will be using a database, so we need to be able to connect to it.
Graphql-yoga essentially is an express server with the necessary tools and defaults for easily making it a graphql-server. With things like subscriptions, file uploads, etc.
Prisma is a powerful ORM for NodeJs and Typescript. But more importantly it also works great with graphql projects since it follows the concept of “schema”. This will make our backend type-safe when updating or reading from the database.
Nexus allows to have declarative, code-first and strongly typed GraphQL schema construction for TypeScript & JavaScript. This schema is used by the server as the client-server agreement, thus defining what and how user interacts with the data.
Husky, lint and prettier will help us format the source code when a new commit is made.

Open your project in the terminal and type: npx prisma init
Now, on an .env file at the root directory add a db connection string like this:

DATABASE_URL=”postgresql://username:pass@localhost:5432/exampledb”

On file prisma/schema.prisma add the following at the end:

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
}

model Recipe {
id String @id @default(cuid())
name String
content String
ingredients Ingredient[]
ratings Rating[]
updatedAt DateTime @default(now()) @updatedAt
createdAt DateTime @default(now())
}

model Ingredient {
id String @id @default(cuid())
name String
recipes Recipe[]
}

model Rating {
recipeId String
recipe Recipe @relation(fields: [recipeId])
comment String
score Int
createdAt DateTime @default(now())

@@id([recipeId, createdAt])
}

Then run: npx prisma migrate dev — preview-feature
This will generate the tables and relationships you need on the db specified at the .env file. Basically each model represents a table, and their fields be simple scalar types or even include a relationship with other models. To learn more about prisma schema, read prisma docs.
After running this command, prisma will start managing your db changes/history with sql files created under prisma/migrations. So, each time you make a significant schema update run the same command (DEV ONLY).
To see the other options run: npx prisma migrate — help

This API will be for having public recipes, which can share ingredients with other recipes and even receive ratings with comments.

To setup your server, let’s first create the folder src/clients and inside of it create prisma.ts and export it with an index.ts file. This, will allow to use it not only for our api context but anywhere in the project (ex. utils).

import { PrismaClient } from "@prisma/client";export const prisma = new PrismaClient();

Create the file src/context.ts which will have the logic for structuring the data that will be passed to each model/node of our API.

import { PrismaClient } from "@prisma/client";
import { ContextParameters } from "graphql-yoga/dist/types";
import { prisma } from "./clients";

export interface Context {
prisma: PrismaClient;
request: any;
response: any;
}

export function createContext(request: ContextParameters) {
return {
...request,
prisma,
};
}

Create the file src/schema.ts which will take your exports from a folder (src/resolvers) and the types generated by the “@prisma/client” package. The output of this will be the graphql schema and the necessary types for your source code. The “nexusPrisma” plugin allows to pass options, this time we are going to tell nexus to include their CRUD operations (optional) for not having to define everything from scratch.

import { makeSchema } from "nexus";
import { nexusPrisma } from "nexus-plugin-prisma";
import * as types from "./resolvers";

export const schema = makeSchema({
types,
plugins: [
nexusPrisma({ experimentalCRUD: true, paginationStrategy: "prisma" }),
],
outputs: {
schema: __dirname + "/../schema.graphql",
typegen: __dirname + "/generated/nexus.ts",
},
contextType: {
module: require.resolve("./context"),
alias: "Context",
export: "Context",
},
sourceTypes: {
modules: [
{
module: "@prisma/client",
alias: "client",
},
],
},
nonNullDefaults: {
output: true,
},
});

Create the file src/server.ts which will make use of graphql-yoga, our context and schema for creating the graphql-server.

import { GraphQLServer, Options } from "graphql-yoga";
import { createContext } from "./context";
import { schema } from "./schema";

const options: Options = {
port: process.env.PORT || 4000,
endpoint: "/",
playground: "/playground",
};

const server = new GraphQLServer({
schema,
context: (ctx) => createContext(ctx),
});

server.start(options, ({ port, endpoint }) => {
console.log(`🚀 Server ready at http://localhost:${port}${endpoint}`);

// Terminate app if needed
process.on("SIGTERM", () => process.exit());
});

Under the src folder create folders for models, mutations and queries.
Each folder (including resolvers) should export everything (with index.ts files).

Create the following model files and export them:

// Ingredient.ts
import { objectType } from "nexus";

export const Ingredient = objectType({
name: "Ingredient",
definition(t) {
t.model.id();
t.model.name();
t.model.recipes();
},
});
// Rating.ts
import { objectType } from "nexus";

export const Rating = objectType({
name: "Rating",
definition(t) {
t.model.recipe();
t.model.comment();
t.model.score();
t.model.createdAt();
},
});
// Recipe.ts
import { objectType } from "nexus";

export const Recipe = objectType({
name: "Recipe",
definition(t) {
t.model.id();
t.model.name();
t.model.content();
t.model.ingredients();
t.model.ratings();
t.model.updatedAt();
t.model.createdAt();
},
});

After this, go to the package.json file and add the following scripts:

"predev": "npm -s run generate:prisma",
"dev": "NODE_ENV=development ts-node-dev -r dotenv/config --no-notify --respawn --transpile-only src/server",
"start": "node dist/server",
"build": "npm -s run clean && npm -s run generate && tsc",
"clean": "rm -rf dist",
"generate": "npm -s run generate:prisma && npm -s run generate:nexus",
"generate:prisma": "prisma generate",
"generate:nexus": "ts-node --transpile-only src/schema",
"db:save": "dotenv -- prisma migrate dev --preview-feature",
"db:deploy": "dotenv -- prisma migrate deploy --preview-feature",
"studio": "dotenv -- prisma studio",
"seed": "ts-node -r dotenv/config prisma/seed.ts"

We are not going to use all of these now, but they even include things for production.

On your terminal run the “dev” script. When the url appears printed, you should be able to open http://localhost:4000/playground on your browser and see a playground app, this can help you on development for reading and testing the api mutations, queries and subscriptions.

Create the file src/resolvers/mutations/index.ts

import { extendType } from "nexus";

export const mutations = extendType({
type: "Mutation",
definition(t) {
t.crud.createOneIngredient({ alias: "createIngredient" });
t.crud.createOneRating({ alias: "rateRecipe" });
t.crud.createOneRecipe({ alias: "createRecipe" });
},
});

Create the file src/resolvers/queries/index.ts

import { extendType, nullable } from "nexus";

export const queries = extendType({
type: "Query",
definition(t) {
t.crud.recipes();
t.crud.recipe();
t.crud.ingredients();
},
});

REMEMBER TO EXPORT ALL THE CONTENT OF THE RESOLVERS FOLDER!!

If you have done everything correctly you should now be able to update the playground on the browser, click the docs tab and see the queries and mutations that we just added.

Create the file prisma/seed.ts

import { prisma } from "../src/clients";async function main() {
const tacos = await prisma.recipe.create({
data: {
name: "tacos de pastor",
content: `
1) Get tortilla
2) Cook some good meat
3) Prepare the sauce
4) Serve with onions and cilantro
`,
ingredients: {
create: [
{
name: "tortillas",
},
{
name: "carne de pastor",
},
{
name: "red sauce",
},
{
name: "onions",
},
{
name: "cilantro",
},
],
},
ratings: {
create: [
{
comment: "Viva México!",
score: 10,
createdAt: new Date("2020-06-22 10:41:53"),
},
{
comment: "Great recipe",
score: 10,
createdAt: new Date("2020-12-25 09:55:11"),
},
],
},
},
include: {
ingredients: true,
},
});
const quesadilla = await prisma.recipe.create({
data: {
name: "quesadilla",
content: `
1) Put tortilla on the heat
2) Place some cheese on top
3) Add some ham
4) Close quesadilla and serve
`,
ingredients: {
connect: {
id: tacos.ingredients[0].id,
},
create: [{ name: "cheese" }, { name: "ham" }],
},
ratings: {
create: [
{
comment: "I love it, tastes great",
score: 10,
createdAt: new Date("2020-03-28 00:57:56"),
},
{
comment: "Great! super simple",
score: 10,
createdAt: new Date("2020-05-16 03:29:43"),
},
{
comment: "Hmm not vegetarian",
score: 2,
createdAt: new Date("2020-06-16 21:17:36"),
},
],
},
},
});
}
main().finally(async () => {
await prisma.$disconnect();
});

In other terminal run the seed script, and then you can see the example data within the playground by using the following query.

query {
recipes {
id
content
name
ratings {
comment
score
}
}
}

You can now create or get recipes, ingredients and ratings!
As you may have noticed we are using the auto-generated crud operations given by nexus. But.. what if we want something more complex or just have more control over the queries? Easy peasy! Let’s think about being able to know the best recipe based on the rating score.

On src/resolvers/queries/index.ts inside the definition, add a new query…

t.field("bestRecipe", {
type: nullable("Recipe"),
description: "Get the best rated recipe",
resolve: async (parent, args, ctx) => {
const [bestRecipe] = await ctx.prisma.$queryRaw(`
SELECT "recipeId", SUM(score) FROM "Rating" as r
GROUP BY "recipeId" LIMIT 1
`);
return ctx.prisma.recipe.findUnique({
where: {
id: bestRecipe.recipeId,
},
});
},
});

Within the playground you can now get the best recipe! With Prisma we can define our own queries and mutations instead of using the auto-generated CRUD. In fact, I would recommend to replace the auto-generated operations so that the queries follow your own convention and desired behavior. Furthermore, if the library methods do not fit what we need is also possible to use SQL with the queryRaw method just like in the example above, just keep in mind that it can put the type-safety at risk if not used carefully.

That’s it! Now you have a working GraphQL API for a recipes app. Feel free to extend the project however you want.

Source code: https://github.com/victormidp/recipe-graphql-api

--

--

Victor Iris

Sr. FullStack Engineer at DigitalOnUs. Passionate about coding new things for a better tomorrow. Experience with PHP, JavaScript, C++ and more.