Prisma is a Node.js and Typescript ORM for databases and features an intuitive data model, automated migrations, type-safety & auto-completion. You would typically define your models and relationships in a schema.prisma file and prisma would expose a data API that enables you to communicate with your database.
Imagine a scenario where you have a postgres database and you want to use graphql (appsync in this case) to communicate with your database, you want to be able to define your models using prisma and run a command that helps you generate graphql schema and resolvers that can communicate with your database and return you data.
That's where prisma-appsync comes in.
Prisma-Appsync lets you instantly generate a full blown Appsync compatible schema from your prisma schema. It lets you iterate fast by auto generating resolvers for CRUD operations based on your prisma schema. You can also define custom resolvers that leverage prisma's API.
Let's explore this by taking a look at a sample prisma.schema
file
// prisma.schemadatasource db {provider = "postgresql"url = env("DATABASE_URL") // The connection string of your postgres database}generator client {provider = "prisma-client-js"binaryTargets = ["native", "rhel-openssl-1.0.x"]}generator appsync {provider = "prisma-appsync"}model Posts {id String @id @default(uuid())title StringuserId Stringuser user @relation(fields: [userId], references: [id])}model user {id String @id @default(uuid())title Stringposts Posts[]}
The schema above defines a post and a user model. Generally when creating models, you would generally want to perform CRUD operations. An example of some of these operations could be:
createUser mutation
updateUser mutation
getUser query
deleteUser mutation
and vice versa for the post model.
This is where prisma-appsync is useful as it automatically provisions resolvers that handle these CRUD operations and generates Appsync compatible schema.
Now that you have a better understanding of what prisma-appsync does, let's see how you can actually use it.
To install prisma-appsync, run the command below:
# using yarnyarn add prisma-appsync# using npmnpm i prisma-appsync
Run the following command to generate the prisma client. You should run this command from the same directory in which your prisma.schema
file is located.
npx prisma generate
You should see a prisma/generated folder in your base directory.
prisma/generated/|__ client # prisma client information|__ resolvers.yaml # list of appsync resolvers|__ schema.gql # Appsync compatible schema
Let's first create our Appsync API stack.
We would deploy a new appsync API using AWS CDK and also create a new lambda functions which would be our API data source.
// cdk/stack.tsimport * as cdk from "aws-cdk-lib";import * as AppSync from "aws-cdk-lib/aws-appsync";import { Construct } from "constructs";import path = require("path");import * as fs from "fs";import * as yaml from "js-yaml";import * as lambda from "aws-cdk-lib/aws-lambda";interface AppsyncStackProps {resolverLambda: lambda.IFunction;}const resolversFileContent = fs.readFileSync("./prisma/generated/prisma-appsync/resolvers.yaml", // The path to the generated resolvers.yaml file"utf8");const loadResolvers = (path: string) =>yaml.load(path) as {typeName: string;fieldName: string;dataSource: string;}[];class AppsyncStack extends cdk.Stack {constructor(scope: Construct,id: string,props?: cdk.StackProps) {super(scope, id, props);const api = new AppSync.GraphqlApi(this, "API", {name: `API`,xrayEnabled: true,authorizationConfig: {defaultAuthorization: {authorizationType: AppSync.AuthorizationType.API_KEY,},},logConfig: {excludeVerboseContent: false,fieldLogLevel: AppSync.FieldLogLevel.ALL,},definition: AppSync.Definition.fromFile(path.resolve(".prisma/generated/prisma-appsync/schema.gql" // The path to the generated schema.gql file)),});const lambdaDataSource = new lambda.Function(this,`ResolverLambda`,{functionName: `ResolverLambda`,runtime: lambda.Runtime.NODEJS_18_X,handler: "index.handler",timeout: cdk.Duration.seconds(30), // Increase the timeout since the lambda would be used to resolve queriescode: lambda.Code.fromAsset("./resolverLambda"), // The path to the lambda function directory});}}
Paste the following code in the index.js
file of the lambda function you created above:
// resolverLambda/index.ts// Import generated Prisma-AppSync client (adjust path as necessary)import { PrismaAppSync } from './prisma/generated/prisma-appsync/client'// Instantiate Prisma-AppSync Clientconst prismaAppSync = new PrismaAppSync()// Lambda handler (AppSync Direct Lambda Resolver)export const main = async (event: any) => {return await prismaAppSync.resolve({ event })}
In the above code, we have been able to create a new Appsync API and the lambda function that would act as the data source for the API. Now let's attach the resolvers generated by prisma-appsync.
// cdk/stack.tsimport * as cdk from "aws-cdk-lib";import * as AppSync from "aws-cdk-lib/aws-appsync";import { Construct } from "constructs";import path = require("path");import * as fs from "fs";import * as yaml from "js-yaml";import * as lambda from "aws-cdk-lib/aws-lambda";interface AppsyncStackProps {resolverLambda: lambda.IFunction;}const resolversFileContent = fs.readFileSync("./prisma/generated/prisma-appsync/resolvers.yaml", // The path to the generated resolvers.yaml file"utf8");const loadResolvers = (path: string) =>yaml.load(path) as {typeName: string;fieldName: string;dataSource: string;}[];class AppsyncStack extends cdk.Stack {constructor(scope: Construct,id: string,props?: cdk.StackProps) {super(scope, id, props);const api = new AppSync.GraphqlApi(this, "API", {name: `API`,xrayEnabled: true,authorizationConfig: {defaultAuthorization: {authorizationType: AppSync.AuthorizationType.API_KEY,},},logConfig: {excludeVerboseContent: false,fieldLogLevel: AppSync.FieldLogLevel.ALL,},definition: AppSync.Definition.fromFile(path.resolve(".prisma/generated/prisma-appsync/schema.gql" // The path to the generated schema.gql file)),});const lambdaDataSource = new lambda.Function(this,`ResolverLambda`,{functionName: `ResolverLambda`,runtime: lambda.Runtime.NODEJS_18_X,handler: "index.handler",timeout: cdk.Duration.seconds(30), // Increase the timeout since the lambda would be used to resolve queriescode: lambda.Code.fromAsset("./resolverLambda"), // The path to the lambda function directory});const lambdaDs = api.addLambdaDataSource(`${id}ResolverLambda-${environment}`,lambdaDataSource);const resolvers = loadResolvers(resolversFileContent);resolvers.forEach(({ typeName, fieldName }) => {const resolverId = `${typeName}${fieldName}Resolver`;new AppSync.Resolver(this, resolverId, {api: api,dataSource: lambdaDs,typeName,fieldName,});});}}
We are importing the resolvers from the resolvers.yaml
file located in the prisma/generated
folder and creating new resolvers for the API as well as attaching the lambda as the data source.
In the lambda function code we wrote, the prismaAppSync.resolve
function automatically handles the resolution of the queries and mutations that were auto-generated as listed in the resolvers.yaml
file.
You can run cdk deploy
to provision your appsync API.
Prisma-appsync also you define custom resolvers/schema and write your logic to interact with your database. You just need to edit your resolver lambda function and add the following:
// resolverLambda/index.tsimport { PrismaAppSync, QueryParamsCustom } from './prisma/generated/prisma-appsync/client'// Instantiate Prisma-AppSync Clientconst prismaAppSync = new PrismaAppSync()// Lambda handler (AppSync Direct Lambda Resolver)export const main = async (event: any) => {return await prismaAppSync.resolve({event,resolvers: {customResolver: async ({args, // Holds the arguments you passed in the graphql callprismaClient // Holds a reference to your prisma client}: QueryParamsCustom<any>) => {// Perform your operation here and return the result};}})}
You would also need to create a custom-resolvers.yaml
and custom-schema.gql
file in the same directory as your prisma.schema
file.
# custom-resolvers.yaml- typeName: MutationfieldName: customResolver # The name of the custom resolverdataSource: prisma-appsync
# custom-schema.gqlextend type Mutation {"""My custom resolver"""customResolver(args: String): String # Specify the arguments and return type of your custom resolver}
You need to let the prisma.schema be aware of the new changes. Update the file with the code below
// prisma.schemagenerator appsync {provider = "prisma-appsync"extendSchema = "./custom-schema.gql"extendResolvers = "./custom-resolvers.yaml"}
Run npx prisma generate
once more to register your new changes.
Then run cdk deploy
so that your lambda resolver is updated with the latest changes.
Just like we have demonstrated, Prisma-Appsync lets you instantly generate a full blown Appsync compatible schema from your prisma schema. It lets you iterate fast by auto generating resolvers for CRUD operations based on your prisma schema. You can also define custom resolvers that leverage prisma's API.
And there you have it folks.
I hope you've learnt something today and I'll be happy to receive feedback from you, trust me any feedback is welcome.
You can also reach me on Twitter or shoot me an email at david.ajayi.anu@gmail.com.
Thanks for reading! 🙂