Building a GraphQL CRUD API for your Database with TypeGraphQL & Prisma

ruheni

Ruheni Alex

Posted on February 25, 2021

Building a GraphQL CRUD API for your Database with TypeGraphQL & Prisma

Building CRUD APIs can be a tedious chore. The time spent writing glue code, plumbing layers together and doing repetitive work is often better invested into tasks that actually add value and solve interesting problems.

In this article, you'll explore how you can prototype an e-commerce GraphQL CRUD API using TypeGraphQL, Apollo Server and Prisma for database access. SQLite will be the database of choice in this tutorial because of its ease of setup. Feel free to use your choice database - Prisma currently supports PostgreSQL, MySQL, SQL Server, SQLite, MongoDB, and CockroachDB.

The complete example used in this tutorial is available on GitHub.

What is TypeGraphQL?

TypeGraphQL is a framework that follows a code-first and object-oriented approach towards building GraphQL APIs. It leverages TypeScript by using classes and decorators.

Note: If you're not familiar with TypeScript yet, check out Learn TypeScript: A Pocketguide Tutorial.

What is Prisma?

Prisma is an ORM - Object-relational mapper. It provides a declarative way to define your database models that are easy to read and understand. It also offers a type-safe database client from your database schema that is intuitive and fun. πŸ™‚

Prerequisites

Before getting started, ensure you have the following:

  • Installed Node.js.
  • Basic understanding of JavaScript/TypeScript.
  • Familiarity with GraphQL APIs.

Note: To get started on learning the basics of GraphQL, How To GraphQL will give you a great foundation.

Step 1: Initialize Your Project

The first step will be creating your working directory on your computer and initialize it by running npm init -y. This command will generate a package.json file with prepopulated values.

mkdir crud-api
cd crud-api
npm init -y
Enter fullscreen mode Exit fullscreen mode

Set Up Dependencies

Install the following dependencies in your project:

npm install apollo-server type-graphql reflect-metadata class-validator
Enter fullscreen mode Exit fullscreen mode

reflect-metadata allows you to do runtime reflection on types.

class-validator is a dependency used by TypeGraphQL to validate inputs and arguments using decorators.

Your project will be dependent on the following development dependencies:

npm install --save-dev typescript ts-node-dev ts-node @types/node @types/ws
Enter fullscreen mode Exit fullscreen mode

The typescript and ts-node-dev dependencies are required to transpile your TypeScript files and restart the app server when a change is made to a file.

A Little More πŸ”§

Create a tsconfig.json file in your project:

touch tsconfig.json
Enter fullscreen mode Exit fullscreen mode

tsconfig.json allows you to define options that will let the TypeScript compiler take advantage of when transpiling your code to JavaScript.

Paste in the following code to your tsconfig.json file:

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "lib": [
      "es2018",
      "esnext.asynciterable"
    ],
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
Enter fullscreen mode Exit fullscreen mode

To take advantage of ts-node-dev's live reloading, modify scripts in your package.json to this script:

// package.json
"scripts": {
  "dev": "ts-node-dev --no-notify --respawn --transpile-only src/index.ts"
},
Enter fullscreen mode Exit fullscreen mode

Next, create your src directory - this will house the bulk of your application - and create an index.ts file as well:

mkdir src
touch src/index.ts
Enter fullscreen mode Exit fullscreen mode

The following code will be your initial setup of your GraphQL server:

// index.ts
import 'reflect-metadata'
import * as tq from 'type-graphql'
import { ApolloServer } from 'apollo-server'

const app = async () => {
  const schema = await tq.buildSchema({})

  new ApolloServer({ schema }).listen({ port: 4000 }, () =>
    console.log('πŸš€ Server ready at: <http://localhost:4000>')
  )
}

app()

Enter fullscreen mode Exit fullscreen mode

VS Code's TypeScript compiler will throw an error about the buildSchema missing the resolver argument. Not to worry, this will be covered in Step 3: Set up typegraphql-prisma. πŸ™‚

Step 2: Set Up Prisma

Now that that's out of the way let's get to the fun part, data modeling.

Initialize Prisma

You'll need to setup Prisma in your project by running the following command:

npx prisma init
Enter fullscreen mode Exit fullscreen mode

The Prisma CLI creates a .env file in your project's root and a new prisma directory. The folder created will contain a schema.prisma file that defines your database connection, the Prisma Client generator and this is where you will define your database models.

The default database source is PostgreSQL. However, you can switch this to your preferred provider - sqlite, mysql, sqlserver, cockroachdb or mongodb - and modify the url pointing to your database. For ease of setup, this tutorial will use SQLite.

Navigate to schema.prisma file and change the datasource provider and datasource url string values to the following:

// schema.prisma
generator client {
  provider = "prisma-client-js"
}

// change the code below πŸ‘‡
datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}
Enter fullscreen mode Exit fullscreen mode

Alternative database connection string setup

In your .env file, add this line:

#.env file
DATABASE_URL="file:./dev.db"
Enter fullscreen mode Exit fullscreen mode

Then modify the datasource url to point to DATABASE_URL variable in your .env file.

// schema.prisma
generator client {
  provider = "prisma-client-js"
}


datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}
Enter fullscreen mode Exit fullscreen mode

The database doesn’t currently exist. Prisma will create the database in prisma/dev.db when running your first migration.

Database Models

For the e-commerce API, you will define 4 models: Product, Category, Order, and User. The relationships between these entities will be as follows:

  • one-to-many relationship between User and Order.
  • many-to-many relationship between Product and Category.
  • many-to-many relationship between Order and Product.

Here is a visual diagram representing the relationships between the different database models:

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gtv3my83usnfcjz0t0zv.png

Your Prisma Schema

With the relationships defined, your final database models should be similar to the one below:

// schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model Product {
  id          String     @id @default(uuid())
  createdAt   DateTime   @default(now())
  updatedAt   DateTime   @updatedAt
  name        String
  sku         String     @unique
  description String?
  quantity    Int
  categories  Category[]
  orders      Order[]
}

model Category {
  id        Int       @id @default(autoincrement())
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  name      String
  products  Product[]
}

model Order {
  id        String    @id @default(uuid())
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  userId    Int?
  customer  User?     @relation(fields: [userId], references: [id])
  products  Product[]
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  email     String   @unique
  firstName String?
  lastName  String?
  address   String?
  orders    Order[]
}
Enter fullscreen mode Exit fullscreen mode

Feel free to modify the fields provided to your preference.

Your First Migration

Next, you'll sync your schema with your database with the following command:

npx prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

The above command creates a database migration and executes it against your database. A database migration refers to incremental, reversible changes that are made to a database schema over time. In that sense, you can think of migrations as a tool enabling "version control" for your database.

The command creates a migration called init. Once the migration is complete, a new /prisma/migrations directory is created. Since you're working with SQLite, Prisma CLI will create your database and apply the changes against your database.

After the first migration is created, the Prisma CLI installs the @prisma/client package. In subsequent database migrations, the Prisma CLI will generate your Prisma Client. Prisma Client is an auto-generated database client that allows you to interact with your database in a type-safe way.

Pretty neat, right? 😜

Step 3: Modify Your GraphQL Server

Create an instance of PrismaClient and add it to the context of your GraphQL Server as follows:

//index.ts
import 'reflect-metadata'
import * as tq from 'type-graphql'
import { ApolloServer } from 'apollo-server'
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient()

const app = async () => {
  const schema = await tq.buildSchema({})

  const context = {
    prisma
  }

  new ApolloServer({ schema, context }).listen({ port: 4000 }, () =>
    console.log('πŸš€ Server ready at: <http://localhost:4000>')
  )
}

app()
Enter fullscreen mode Exit fullscreen mode

Adding prisma to your context makes sure that PrismaClient is made available to your GraphQL operations.

Step 4: Set Up typegraphql-prisma

TypeGraphQL provides an integration with Prisma using the [typegraphql-prisma](https://www.npmjs.com/package/typegraphql-prisma) package. This package will auto-generate types, enums and even CRUD resolvers based on your Prisma schema, which you otherwise would need to write manually.

Install typegraphql-prisma as a dev dependency with the following command:

npm install --save-dev typegraphql-prisma
Enter fullscreen mode Exit fullscreen mode

typegraphql-prisma is dependent on several packages for it to work properly.

npm install graphql-type-json graphql-fields
npm install --save-dev @types/graphql-fields
Enter fullscreen mode Exit fullscreen mode

Once the dependencies are successfully installed, add a new generator in your schema.prisma file below the client generator as follows:

// schema.prisma
generator client {
  provider = "prisma-client-js"
}

generator typegraphql {
  provider = "typegraphql-prisma"
}
Enter fullscreen mode Exit fullscreen mode

Run npx prisma generate to generate TypeGraphQL classes and CRUD resolvers based on your Prisma schema.

npx prisma generate
Enter fullscreen mode Exit fullscreen mode

typegraphql-prisma emits the generated classes, enums and resolvers to node_modules/@generated/typegraphql-prisma.

It should be noted that in your subsequent migrations, the classes and resolvers TypeGraphQL generates will be automatically updated on running npx prisma migrate dev.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zby23fodrm0pp7phks67.png

The final step is updating our resolvers in our Apollo Server:

// index.ts
import 'reflect-metadata';
import { PrismaClient } from '@prisma/client';
import { ApolloServer } from 'apollo-server';
import { resolvers } from "@generated/type-graphql";
import * as tq from 'type-graphql';

const prisma = new PrismaClient()

const app = async () => {
  const schema = await tq.buildSchema({ resolvers })

  const context = () => {
    return {
      prisma
    }
  }

  new ApolloServer({ schema, context }).listen({ port: 4000 }, () =>
    console.log('πŸš€ Server ready at: <http://localhost:4000>')
  )
}

app()
Enter fullscreen mode Exit fullscreen mode

Step 5: Test Your New GraphQL API

Start your GraphQL server with the following command:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open localhost:4000 to interact with the GraphQL playground...and congratulations. πŸŽ‰

You've successfully automated building a CRUD API. Just to be sure explore Docs section of the playground and try out some queries and mutations:

Create a New Category:

mutation {
  createCategory(data: { name: "electronics" }) {
    id
    name
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a New Product:

mutation {
  createProduct(
    data: {
      name: "Google Pixel"
      sku: "90456123098"
      quantity: 1000
      categories: { connect: [{ id: 1 }] }
    }
  ) {
    id
    name
  }
}

Enter fullscreen mode Exit fullscreen mode

Query all Products:

{
  products {
    id
    name
    quantity
    sku
    categories{
      name
    }
    orders{
      id
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Everything is running smoothly. Go ahead and pat yourself on the back for getting this far.

https://media.giphy.com/media/U7DFwka0lRgiIWKZnP/giphy.gif

Feel free to explore other mutations and queries on the playground. πŸ™‚

Side Quest: Some typegraphql-prisma Wizardry πŸ§™πŸΎβ€β™‚οΈ

Alternatively, you can make specific CRUD operations, specific actions, modify generated functions that can be performed against your Prisma schema. This gives you more control of the operations exposed on your API. This approach also allows you to create your custom resolvers and add them to your GraphQL schema.

Expose Specific Prisma CRUD operations

typegraphql-prisma allows you to expose selected CRUD operations on your API on your Prisma models.

import {
  ProductCrudResolver,
  CategoryCrudResolver
} from "@generated/type-graphql";

const schema = await tq.buildSchema({
  resolvers: [
    ProductCrudResolver,
    CategoryCrudResolver
  ]
})

Enter fullscreen mode Exit fullscreen mode

Expose Specific Prisma Actions

To get more control over the GraphQL operations, you can expose, typegraphql-prisma allows you to expose specific Prisma operations from the generated types.

import {
  CreateProductResolver,
  UpdateProductResolver
} from "@generated/type-graphql";

const schema = await tq.buildSchema({
  resolvers: [
    CreateProductResolver,
    UpdateProductResolver
  ]
})

Enter fullscreen mode Exit fullscreen mode

TypeGraphQL allows you to add custom queries and mutations to your schema using the generated Prisma Client.

Applying Custom Resolvers

The beauty of TypeGraphQL is that you can create custom resolvers such as authorization by creating its decorators.

typegraphql-prisma generates the applyResolversEnhanceMap function and ResolversEnhanceMap to aid in the creation of a config object that allows you to add decorator functions.

import {
  ResolversEnhanceMap,
  applyResolversEnhanceMap,
} from "@generated/type-graphql";
import { Authorized } from "type-graphql";

const resolversEnhanceMap: ResolversEnhanceMap = {
  Category: {
    createCategory: [Authorized(Role.ADMIN)],
  },
};

applyResolversEnhanceMap(resolversEnhanceMap);

Enter fullscreen mode Exit fullscreen mode

Learn more about other advanced operations you can apply to your GraphQL resolvers, such as custom resolvers, authorization, middleware and additional decorators to your Prisma schema and models here.

Generating the SDL

To enable viewing the SDL of your GraphQL API, make the following modification in your application:

  const schema = await tq.buildSchema({
    resolvers,
    emitSchemaFile: true
  })

Enter fullscreen mode Exit fullscreen mode

This will automatically create a schema.gql file with your GraphQL schema definitions in your working directory.

You could also specify the exact path for your schema.gql file. This modification will add it in src/snapshots/schema folder:

import path = require('path');

const schema = await tq.buildSchema({
    resolvers,
        emitSchemaFile: path.resolve(__dirname, "snapshots/schema", "schema.gql"),
 })
Enter fullscreen mode Exit fullscreen mode

For the application you just built, here's a preview of the type User's queries and mutations that is generated:

type Query {
  aggregateUser(cursor: UserWhereUniqueInput, orderBy: [UserOrderByInput!], skip: Int, take: Int, where: UserWhereInput): AggregateUser!
  findFirstUser(cursor: UserWhereUniqueInput, distinct: [UserScalarFieldEnum!], orderBy: [UserOrderByInput!], skip: Int, take: Int, where: UserWhereInput): User
  user(where: UserWhereUniqueInput!): User
  users(cursor: UserWhereUniqueInput, distinct: [UserScalarFieldEnum!], orderBy: [UserOrderByInput!], skip: Int, take: Int, where: UserWhereInput): [User!]!
}

type Mutation {
  createUser(data: UserCreateInput!): User!
  deleteManyUser(where: UserWhereInput): AffectedRowsOutput!
  deleteUser(where: UserWhereUniqueInput!): User
  updateManyUser(data: UserUpdateManyMutationInput!, where: UserWhereInput): AffectedRowsOutput!
  updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
  upsertUser(create: UserCreateInput!, update: UserUpdateInput!, where: UserWhereUniqueInput!): User!
}

Enter fullscreen mode Exit fullscreen mode

You can learn more about this feature here.

Conclusion

You have successfully built a CRUD GraphQL API by barely writing any code. Simple and fast. I hope you have enjoyed this tutorial. Feel free to share your thoughts, and share what kind of tutorials or examples you would like to see more.

Explore the prisma-examples to see how Prisma can fit in your stack. If you feel an example is missing, create an issue. 😊

Happy hacking! πŸ‘©πŸΎβ€πŸ’»πŸ‘¨πŸΎβ€πŸ’»

πŸ’– πŸ’ͺ πŸ™… 🚩
ruheni
Ruheni Alex

Posted on February 25, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related