Increase Your Productivity By Using Generators for Prisma & GraphQL
Type-safe Prisma & GraphQL generator for the Node.js Ecosystem
Before I started writing server-side code in Typescript, GraphQL, and Node.js, I used Ruby on Rails and Elixir’s Pheonix Framework.
With both Rails and Phoenix, there is a clearly structured way to format and organize serverside code. Both of these frameworks followed the MVC architecture, which made code organization trivial. These frameworks also included a set of CLI tools to increase your productivity. Rails has bin/rails generate
, and Phoenix has mix phx.gen
. Both utilities can generate models, views, and controllers for a given resource, and it saves the developer a lot of time by automatically generating boilerplate code.
As I migrated into Node.js, I noticed that such utilities don’t exist. Maybe that’s because there is no dominant framework that enforces a strict way of code organization (maybe NestJS is an exception).
I got comfortable with a certain way of organizing and developing server-side code in Node.js, Prisma, GraphQL, and Typescript, and decided to help myself (and other developers) by making a CLI generator utility that compliments my workflow.
Welcome Genql
Genql is a CLI utility that automatically generates Prisma models and GraphQL resolvers for a given model. I chose to write it in Go because Go is a well-structured statically typed language, and other popular CLI tools have had success using Go (Hugo, Railway CLI, and many others).
Genql is installable via homebrew if you’re on Mac or Linux. To install, run the following two commands:
$ brew tap tk04/genql
$ brew install genql
This is how it works:
Before using genql, you must have Prisma set up on your project first. To create a model, simply run:
$ genql model [model name] field:type:default_value/"unique" ....
You can create as many fields as you want, and even generate one-to-one, one-to-many, and many-to-many relationships between models. The third value in the colon-separated argument is reserved for default values. You can also include the “unique” keyword, which will add the “@unique” attribute to a given field.
For example, if we wanted to create a model named “User”, with an auto-incrementing id, name of type string, and an age of type int, you’d run the following command:
$ genql model User id:id:ai name:string age:int
This will add the following code block to your prisma.schema
file:
model User {
id Int @id @default(autoincrement())
name String
age Int
}
In id:id:ai
, “id” is the field name, “id” is the type, and “ai” (stands for auto increment) dictates the type of the id field and the default value. The other viable entry for an id type is “id:id:uuid”
, which generates an Id of type “String”, with a default value of a UUID string.
Genql also supports optional values. For example, if you wanted to make the name value from the previously created model optional, you’d do:
$ genql model User id:id:ai name:"string?" age:int
This will generate the following code:
model User {
id Int @id @default(autoincrement())
name String?
age Int
}
To showcase the other features of genql, let’s also create a Friend model, with a default UUID id field, a unique string email field, and a name field of type string. Let’s also establish a one-to-one relationship between the User and the Friend models. Here is the command to do such a task:
$ genql model Friend id:id:uuid email:string:unique name:string --OneToOne user:User
This command will add the following code to your prisma.schema
file:
model User {
id Int @id @default(autoincrement())
name String?
age Int
friend Friend?
}
model Friend {
id String @id @default(uuid())
email String @unique
name String
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
Notice that the command also adjusted the User model to establish the one-to-one relationship between the models.
Other than the model command, genql also supports a “resolvers” command, which will create GraphQL resolvers for a given Prisma model. This command makes a lot of assumptions about your current code organization and structure, so it’s not for everyone. It assumes that you use Type-GraphQL, and organize your resolvers under appname/src/resolvers/
. The command also creates a types.ts
file with each resolver that includes input & output types to be used in the queries and mutations.
To showcase using it on the Friend model we created previously, run:
$ genql resolvers Friend
This will create 2 files, both under appname/src/resolvers/Friend
. The Index.ts
file will contain the queries & mutations, while the types.ts
file will contain the types. The “resolvers” command is supposed to be used as a basic entry point to get you started with developing your resolvers. So you’re expected to add error handling and adjust the generated code depending on your needs.
This is what the generated Index.ts file will look like:
import { Arg, Ctx, Mutation, Query, Resolver } from "type-graphql";
import { context } from "../context";
import { Friend, createFriendInput, updateFriendInput } from "./types";
@Resolver()
export class FriendResolver {
@Mutation(() => Friend, { nullable: true })
deleteFriend(@Ctx() { prisma }: context, @Arg("id") id: string) {
return prisma.friend.delete({
where: {
id: id,
},
});
}
@Query(() => Friend, { nullable: true })
getFriend(@Ctx() { prisma }: context, @Arg("id") id: string) {
return prisma.friend.findFirst({
where: {
id: id,
},
});
}
@Mutation(() => Friend)
createFriend(
@Ctx() { prisma }: context,
@Arg("input") input: createFriendInput
) {
return prisma.friend.create({
data: {
...input,
},
});
}
@Mutation(() => Friend)
updateFriend(
@Ctx() { prisma }: context,
@Arg("input") input: updateFriendInput
) {
return prisma.friend.update({
where: { id: input.id },
data: {
...input,
},
});
}
}
And this is what the generated types.ts file will look like:
import { Field, InputType, ObjectType } from "type-graphql";
@ObjectType()
export class Friend {
@Field({ nullable: true })
id?: string;
@Field()
email: string;
@Field()
name: string;
@Field()
userId: number;
}
@InputType()
export class createFriendInput {
@Field({ nullable: true })
id?: string;
@Field()
email: string;
@Field()
name: string;
@Field()
userId: number;
}
@InputType()
export class updateFriendInput {
@Field()
id: string;
@Field({ nullable: true })
email?: string;
@Field({ nullable: true })
name?: string;
@Field({ nullable: true })
userId?: number;
}
Hopefully, some of you find this utility helpful. If you find any issues or have any feature suggestions, I’m very open to contributions. The full source code can be found here: https://github.com/tk04/genql