As part of my learning everything there is to know about Prisma I've decided to write some tutorials for you as much for me. There may be some knowledge gaps and I may not have a great grasp on some things yet, so if you find an error or something that I have explained that is fundamentally wrong, feel free to message me on Twitter @studio_hungry and we can duke it out!
What you will build
You are going to build a GraphQL api backed by a postgres database. This api will hold data for showing different competencies within companies. For example, each company will have roles within their organization (Software Engineer, Cloud Specialist) which require certain skills (Scala, React).
Once complete you will have learnt how to setup your own GraphQL api, how to use Prisma as an ORM layer and how to use Nexus as a means of producing code first GraphQL schemas.
Prerequisites
This tutorial assumes basic knowledge of databases, APIs and how to setup a project and install packages. The tutorial borrows heavily from the awesome docs for Prisma and Nexus.
What is Prisma and Nexus?
Prisma is an ORM (Object-Relational-Mapping) layer that sits on top of your database. It ships with a query builder which allows you to write type safe queries as JavaScript (or TypeScript) objects. It's built to work with TypeScript, has it's own vscode extension and offers a lovely abstraction over SQL databases. For a full (and in depth) rundown of what Prisma is you can check out their docs page, Why Prisma?.
Nexus is a declarative, code first approach to writing GraphQL schemas. It's created by a team at Prisma and works seamlessly with it. Nexus allows you to house your schema and resolver logic in one place and write it with one common language such as JavaScript or TypeScript. You'll learn more about this approach during the tutorial. If you wish to read further you can checkout their excellent docs page Why Nexus?.
Setup the project
Create a new directory in your project location of choice and begin by installing the required packages.
This tutorial will use NPM to run the application, you can use Yarn if you wish.
npm init -y
npm i --save-dev prisma ts-node-dev typescript
npm i @prisma/client apollo-server graphql nexus
The article uses the following versions:
"devDependencies": {
"prisma": "^2.17.0",
"ts-node-dev": "^1.1.1",
"typescript": "^4.1.5"
},
"dependencies": {
"@prisma/client": "^2.17.0",
"apollo-server": "^2.21.0",
"graphql": "^15.5.0",
"nexus": "^1.0.0"
}
Next create a tsconfig.json
file at your projects root and add the following boilerplate:
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"lib": ["esnext"],
"strict": true,
"rootDir": ".",
"outDir": "dist",
"sourceMap": true,
"esModuleInterop": true
}
}
Finally add some build scripts to the package.json
"dev": "ts-node-dev --transpile-only --no-notify api/app.ts",
"build": "tsc"
Now that everything is installed you'll need to create some files and folders. All the code will live in an api
folder at the projects root. The structure will look like this:
- api
- graphql
- index.ts
- app.ts
- context.ts
- db.ts
- schema.ts
- server.ts
Setup the database
This tutorial uses Postgres as it's database. If you don't already have an instance of Postgres installed on your machine you can grab it from the download page on their site. The default postgres installation comes with the user postgres
. This tutorial will assume that user from here onwards, though you can swap that out for your own user if you wish. This tutorial will also use psql
for interacting with the database at certain times. psql
is a tool for writing SQL queries from the command line, it comes with the installation of Postgres.
Once you have it installed login and create a new database.
// Login
psql -U postgres;
// Create database
CREATE DATABASE companyCompetenceDatabase;
// Confirm database creation
\l;
Adding the Prisma schema data models
To setup Prisma you must first invoke the CLI by running npx prisma
then npx primsa init
. A new folder will be created at the projects root called prisma
which will house a schema.prisma
file. A .env
file will also be created and pre-populated with a placeholder connection string.
The DATABASE_URL
is the connection string that will be used to hook up to your Postgres database. In the .env
file change the placeholder to the following, replacing the password for your own, the user for your own if you have a different one and the database name if you choose a different name.
DATABASE_URL="postgresql://postgres:test1234@localhost:5432/companycompetencedatabase?schema=public"
Note that although you gave the name of the database in camelCase when creating it, Postgres converts that to all lower case once the database is created. I have left the creation string as camelCase for readability.
Inside schema.prisma
add the following models below the client generator.
// 1-n with trade and role
// company can have many roles
model Company {
id Int @id @default(autoincrement())
name String?
contactPerson String?
bio String?
email String?
website String?
roles Role[]
}
// 1-n with skill, n-1 with company
// a role can have many skills
// a company can have many roles
model Role {
id Int @id @default(autoincrement())
name String?
skills Skill[]
company Company? @relation(fields: [companyId], references: [id]) // not in db, used by Prisma to set relation
companyId Int? // the foreign key, this is in the db
}
// 1-n with role
model Skill {
id Int @id @default(autoincrement())
name String?
role Role? @relation(fields: [roleId], references: [id])
roleId Int?
}
Let's dive into what all this means. The Prisma data modal is a schema that maps to your database schema. In it you define the shape of your data and any relationships that may exist between tables. If you have never worked with SQL
databases some of fields may seem confusing. Let's break them down.
In SQL
land relationships between tables are defined by foreign keys. These are ids that map to other tables. In the example above you can see that the Role
model has a field called companyId
, this is a foreign key that will be added to the database and represents the link to the primary key (the id field) in the Company
model. In the Company
model you can see an array of roles. This represents a collection or list of roles, the many in the 1-many relationship that the Company
(1) and Roles
(many) can have. The ?
next to many of the field denotes that they are nullable.
A more explicit example of a one-many relationship can be viewed below:
model One {
id Int @id @default(autoincrement()) // Primary key
hasMany Many[]
}
model Many {
id Int @id @default(autoincrement()) // Primary key
oneRelationField One @relation(field: [foreignKey], references: [id]) // Not in db, used by Prisma to describe relation
foreignKey Int // The foreign key, this is in the db
}
The foreignKey
references the id on the One
model, this tells the database that the primary key from One
is used as the foreignKey on Many
. The foreignKey is designed to allow multiple instances of the value from which it references. Only the foreign key is used in the actual database table, the oneRelationField
is used by Prisma when creating relations.
Now that you have created your data model you can run a migration and map the models to the database schema. The following command creates a new SQL migration file named init
and runs the migration against the database. This creates the tables in the format described by the models.
npx prisma migrate dev --name init --preview-feature
Using the Nexus objectType
The traditional way to setup a GraphQL schema is to build a schema using the GraphQL Schema Language
which is written as a template string. A resolver is also created to resolve the endpoint and return whatever data is requested. In Apollo this is done by creating a typeDefs
file and declaring the schema types and queries. Inside the typeDefs
file you would create the the GraphQL types (which look quite similar to the Prisma Schema Language (PSL)) and queries inside an exported template string on a GraphQL tag, an example how this might look can be seen below:
exports.typeDefs = gql`
type Query {
allCharacters: [Character]!
characterById(_id: ID!): Character
characterByName(name: String!): Character
}
type Character {
_id: ID
name: String!
house: String
patronus: String
bloodStatus: String
role: String
school: String
deathEater: Boolean
dumbledoresArmy: Boolean
orderOfThePheonix: Boolean
ministryOfMagic: Boolean
alias: String
wand: String
boggart: String
animagus: String
}
`
This file would then be added to every time you wanted to introduce a new data type or query definition, this works so long as the number of data models is small but is not very modular and wouldn't be easy to scale if you were to introduce multiple new data types and queries. Another way to accomplish defining your GraphQL types and schema is to use Nexus and to take a code first approach by giving each type its own file and using the objectType
function.
Inside api/graphql
create a new file called company.ts
and add the following:
import { objectType } from 'nexus';
export const Company = objectType({
name: 'Company',
definition(t) {
t.nonNull.int('id');
t.string('name');
t.string('contactPerson');
t.string('bio');
t.string('email');
t.string('website');
t.int('roleId');
t.nonNull.list.nonNull.field('roles', {
type: 'Role',
resolve: (parent, _, ctx) => {
return ctx.db.company
.findUnique({
where: { id: parent.id },
})
.roles();
},
});
},
});
The objectType
function is used to fetch the fields from your schema. It's combined with a resolver and describes the schema models in code. The definition
method allows you to define the fields of your object type as well as the relationships.
Let's dissect the relations field to make sure we understand what is happening here.
t.nonNull.list.nonNull.field('roles', {
type: 'Role',
resolve: (parent, _, ctx) => {
return ctx.db.company
.findUnique({
where: { id: parent.id },
})
.roles();
},
});
The field is not nullable, that is, it can't be null t.nonNull
, it's an array (a list) of values which can also not be null, t.nonNull.list.nonNull
. In the field you have given it a name that corresponds to the name used in the schema and set it's model type, Role
, you then resolve the relationship. The first argument in the resolver is the parent or root node, this is used to pass information from the parent to the child resolvers. The second is the arguments passed to the resolver and the third the schema context, which we will shortly be covering. The Prisma client (via the context) is used to query for the model and find the record where the id matches. So in this case the query will resolve to find zero or one Company that matches the filter.
The full contents of api/GraphQL
// company.ts
export const Company = objectType({
name: 'Company',
definition(t) {
t.nonNull.int('id');
t.string('name');
t.string('contactPerson');
t.string('bio');
t.string('email');
t.string('website');
t.int('roleId');
t.nonNull.list.nonNull.field('roles', {
type: 'Role',
resolve: (parent, _, ctx) => {
return ctx.db.company
.findUnique({
where: { id: parent.id },
})
.roles();
},
});
},
});
// role.ts
export const Role = objectType({
name: 'Role',
definition(t) {
t.nonNull.int('id');
t.string('name');
t.field('company', {
type: 'Company',
resolve: (parent, _, ctx) => {
return ctx.db.role
.findUnique({
where: { id: parent.id || undefined },
})
.company();
},
});
t.list.field('skills', {
type: 'Skill',
resolve: (parent, _, ctx) => {
return ctx.db.role
.findUnique({
where: { id: parent.id || undefined },
})
.skills();
},
});
},
});
// skill.ts
export const Skill = objectType({
name: 'Skill',
definition(t) {
t.nonNull.int('id');
t.string('name');
t.field('role', {
type: 'Role',
resolve: (parent, _, ctx) => {
return ctx.db.skill
.findUnique({
where: { id: parent.id || undefined },
})
.role();
},
});
},
});
Finally, export the types from the api/GraphQL/index.ts
file:
export * from './company';
export * from './role';
export * from './skill';
Creating the server
Begin by opening api/schema.ts
. This will be where you make a schema with Nexus. You will pass in the defined types from the api/GraphQL/index.ts
file and set the output paths. These paths will be where Nexus writes the generated TypeScript definitions that are derived from the Prisma schema. It will also output a GraphQL schema, this file should not be edited directly.
schema.ts
import { makeSchema } from 'nexus';
import { join } from 'path';
// It is considered best practice to pass your types directly from a "star import" like we've done above. Under the hood, Nexus will unwrap the types. This prevents from constantly having to manually export & import every single type of your API.
import * as types from './graphql';
export const schema = makeSchema({
// GraphQL types that will be used to construct your GraphQL schema.
types,
outputs: {
// Output path to where nexus should write the generated TypeScript definition types derived from your schema. This is mandatory to benefit from Nexus' type-safety.
typegen: join(__dirname, '..', 'nexus-typegen.ts'),
// Output path to where nexus should write the SDL (schema definition language) version of your GraphQL schema.
schema: join(__dirname, '..', 'schema.GraphQL'),
},
contextType: {
// Path to the module where the context type is exported
module: join(__dirname, './context.ts'),
// Name of the export in that module
export: 'Context',
},
});
Open the server.ts
file and add the following:
import { ApolloServer } from 'apollo-server';
import { schema } from './schema';
import { context } from './context';
export const server = new ApolloServer({ schema, context });
Here you are instantiating the ApolloServer instance and exporting it while passing in the schema and context. Next you will create the context.
Finally open app.ts
and add the following, which will start the server when the dev
script is run.
import {server} from './server';
server.listen().then(({url}) => {
console.log(`Server ready at ${url}`)
})
The GraphQL context
The GraphQL context is an object which is shared across all the resolvers, the GraphQL server creates and destroys a new instance between each request. To create a context open the api/db.ts
file.
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();
Then the api/context.ts
file
import { db } from './db';
import { PrismaClient } from '@prisma/client';
export interface Context {
db: PrismaClient;
}
export const context = {
db,
};
Now the context has been created and passed to the GraphQL server. The context can be used to store and/or check information too. One such use case might be an authenticated user, are they authorized to do a certain mutation? In any case, your context is a simple object which will suffice for now.
Writing queries and mutations
The API isn't much good if you can't make any requests against it. Begin by opening back up the companies.ts
file. Under the objectType
creation of the Company
type, add the new Query
and Mutation
types.
export const Company = objectType({
name: 'Company',
definition(t) {
t.nonNull.int('id');
t.string('name');
t.string('contactPerson');
t.string('bio');
t.string('email');
t.string('website');
t.int('roleId');
t.nonNull.list.nonNull.field('roles', {
type: 'Role',
resolve: (parent, _, ctx) => {
return ctx.db.company
.findUnique({
where: { id: parent.id },
})
.roles();
},
});
},
});
export const CompanyQuery = extendType({
type: 'Query',
definition(t) {
// get all companies
t.list.field('companies', {
type: 'Company',
resolve(_root, _args, ctx) {
return ctx.db.company.findMany();
},
});
// get company by id
t.field('company', {
type: 'Company',
args: {
id: nonNull(intArg()),
},
resolve(_root, args, ctx) {
return ctx.db.company.findUnique({
where: { id: args.id },
});
},
});
t.list.field('roles', {
type: 'Role',
resolve(_root, _args, ctx) {
return ctx.db.role.findMany();
},
});
},
});
export const CompanyMutation = extendType({
type: 'Mutation',
definition(t) {
// create a new company
t.nonNull.field('createCompany', {
type: 'Company',
args: {
id: intArg(),
name: nonNull(stringArg()),
contactPerson: nonNull(stringArg()),
bio: nonNull(stringArg()),
email: nonNull(stringArg()),
website: nonNull(stringArg()),
roleId: intArg(),
roles: arg({
type: list('RoleInputType'),
}),
},
resolve(_root, args, ctx) {
return ctx.db.company.create({
data: {
name: args.name,
contactPerson: args.contactPerson,
bio: args.bio,
email: args.email,
website: args.website,
roles: {
connect: [{ id: args.roleId || undefined }],
},
},
});
},
});
// update a company by id
t.field('updateCompany', {
type: 'Company',
args: {
id: nonNull(intArg()),
name: stringArg(),
contactPerson: stringArg(),
bio: stringArg(),
email: stringArg(),
website: stringArg(),
roleId: intArg(),
roles: arg({
type: list('RoleInputType'),
}),
},
resolve(_root, args, ctx) {
return ctx.db.company.update({
where: { id: args.id },
data: {
name: args.name,
contactPerson: args.contactPerson,
bio: args.bio,
email: args.email,
website: args.website,
roles: {
connect: [{ id: args.roleId || undefined }],
},
},
});
},
});
// delete a company by id
t.field('deleteCompany', {
type: 'Company',
args: {
id: nonNull(intArg()),
},
resolve(_root, args, ctx) {
return ctx.db.company.delete({
where: { id: args.id },
});
},
});
},
});
Let's dissect that massive block of code. Starting with the Query
block, the queries available to you in the playground are defined using much of the same syntax as when the object itself was defined with fields and resolvers.
// get all companies
t.list.field('companies', {
type: 'Company',
resolve(_root, _args, ctx) {
return ctx.db.company.findMany();
},
});
The query to get all of the companies uses the GraphQL context to run a prisma query and the method findMany()
to return all of the companies in the database. The findMany()
method can take optional parameters which are filters on the query, for example to return only the first 10 company in the database you could pass an object with the key take
and the value 10
like so:
// get all companies
t.list.field('companies', {
type: 'Company',
resolve(_root, _args, ctx) {
return ctx.db.company.findMany({ take: 10 });
},
});
This value could be passed into the query as an argument too instead of being hardcoded. Let's look at passing arguments to queries.
To get a company by it's id
you'll make use of the second paramter of the resolver function, args
.
// get company by id
t.field('company', {
type: 'Company',
args: {
id: nonNull(intArg()),
},
resolve(_root, args, ctx) {
return ctx.db.company.findUnique({
where: { id: args.id },
});
},
});
Here a different method is used on the GraphQL Prisma context, findUnique()
. A filter is passed which asks for only companies where
it's id
matches the id
passed in as a parameter. Above the resolver the args types are defined. Here an id
is defined as a nonNull integer, that is, a number value that cannot be null. Which makes sense as you wont run this query without actually passing a value to it. If you were to not add the nonNull function the arg would be nullable be default. (Though you can also import nullable from nexus and explicitly state that fact that it is nullable). The roles query works the same way as the get all companies query in that it returns all of the roles related to this company.
Moving onto the mutations and creating a new company in the database.
// create a new company
t.nonNull.field('createCompany', {
type: 'Company',
args: {
id: intArg(),
name: nonNull(stringArg()),
contactPerson: nonNull(stringArg()),
bio: nonNull(stringArg()),
email: nonNull(stringArg()),
website: nonNull(stringArg()),
roleId: intArg(),
roles: arg({
type: list('RoleInputType')
})
},
resolve(_root, args, ctx) {
return ctx.db.company.create({
data: {
name: args.name,
contactPerson: args.contactPerson,
bio: args.bio,
email: args.email,
website: args.website,
roles: {
connect: [
{id: args.roleId || undefined},
]
}
}
})
}
})
Much of this code will now be familiar, the field type is given a logical name (which will be used to run the mutation) and a bunch of args are defined outlining what the resolver can expect and in what format the variables will be passed as. An interesting point to notice here is the roleId
, which is the foreign key for the Role
type, and the roles
arg which uses a custom input type to tell the resolver what type to expect. As the Role
isn't a primitive you have to declare a custom type which is used instead. As the plural name suggests, this argument is of type list
. The RoleInputType
is defined in the role.ts
file and defines an id
.
The resolver uses the create method on the Prisma context and defines a data
object which maps the args passed through to the corresponding object keys. The roles arg uses the connect
method and the foreign key to connect the new company to a previously defined role.
Running our queries and mutations in the Playground
In order to actually play about with the queries and mutations run the dev
script from the projects root (npm run dev
). This will open up a url at localhost:4000. The server has given you a GraphQL playground from which you can run your queries and mutations against the database in real time. This is an excellent way to check how your data behaves in the real world, outside of those console.logs
.
Begin by creating a new skill.
mutation CreateSkill {
createSkill(name: "GraphQL") {
id
name
}
}
Here you are running your createSkill
mutation passing in the name of the skill top be created and returning the newly created skills id and name. Next create a role.
mutation CreateRole {
createRole(name: "Backend Developer" skillId: 1) {
id
name
}
}
The createRole
mutation accepts a name
argument and an optional skillId
, which is the foreign key to the skill you would like to link to the role. In this case you are creating a new Backend Developer
role and assigning it the skill of GraphQL
via the skills id.
Finally you can create a new company. By taking advantage of the foreign key (roleId
) you can link not only a role but certain skills to the company. In this mutation your new company has a Backend Developer
role which requires/uses a GraphQL
skill.
mutation CreateCompany {
createCompany(
name: "My Super Cool Company"
bio: "A super cool company"
website: "mycomp.com"
contactPerson: "Barry McBarry"
email: "barry@mycomp.com"
roleId: 1
) {
name
bio
website
contactPerson
email
roles {
name
skills {
name
}
}
}
}
Now that your database has some data in it you can query it to see how that data looks when returned.
# get all companies and their roles and skills
query GetCompanies {
companies {
id
name
contactPerson
bio
email
website
roles {
name
skills {
name
}
}
}
}
#get all roles and their skills
query GetRoles {
roles {
id
name
skills {
name
}
}
}
#get all skills
query GetSkills {
skills {
id
name
}
}
Conclusion
As a predominantly frontend developer using Prisma and combining it with Nexus for the first time has been a relatively painless experience. Of course much of that is due to the excellent documentation which allows you to get up and running quickly and without fuss. Some areas where I did find myself rather confused was nested mutations and relations in the schema. People learn in different ways, I enjoy quite a lot of visuals with pictures forming a better understanding in my head as opposed to just blocks of text. Perhaps the docs could do with some more video instructions and interactive elements. That being said this was quite a text and code block heavy piece itself!
This was the first of many pieces about my journey learning Prisma and what it can do. This article only touched on some base concepts but I found it very helpful in getting a grasp of what is possible. My take away from this piece is that I need to dive deeper into relationships and how they are used as this seems to be a focal point of what you can do with Prisma.
Hit me up on Twitter @studio_hungry if you fancy a chinwag about any of this.
Top comments (1)
Hi, do you have the github repository for this article? There´s missing parts since you only show the Company queries and mutations.
thanks!