An app that allows users to create blog posts.
- Learn GraphQL.
- How to combine GraphQL with NestJS.
- What are resolvers.
- What are mutations.
- What are subscriptions.
- What are scalars.
- What are directives.
- What are plugins.
- What are field middlewares and how to use the with
Code First Approach. - What is the
Code First Approachand how to use it to document the schema. - What is
data loaderand how to avoid theN+1 problem. - What is schema federation and how to use it.
- Learn knex library basics.
- How to create entities.
- How to create SQL relationships (one to one, one to many, many to many).
- How to create database migrations.
- How to create database seeds.
User- Users play a key role in the application. Each person who wants to use the application must create an account. The created account is used for user authentication and authorization purposes, as well as to create relationships with other entities in the application.Profile- Each user has a profile created at the time of account creation. The profile is used for presentation purposes and is responsible for showing more details about the user in the application.Post- Any user in the app can create posts. Posts are visible by all users and are the heart of the app.Comments- Each post can have comments added to it. Comment can only be created by logged-in user.Tags- Post authors have the ability to add tags so that app users can more easily search for posts of interest.
Functionalities that are available in the application.
- Authentication/Authorization
- As a user, I can register an account.
- As a user, I can log in.
- Profiles
- As a user, I have a default profile assigned on account creation.
- As a user, I can update my profile (e.g. change username, change profile picture).
- Posts
- As a user, I can create a post.
- As a user, I can update the post I created.
- as a user, I can read posts by tags.
- Comments
- As a user, I can create a comment.
- As a user, I can update the comment I created.
- Tags
- As a user, I can create a tag.
- As a user, I can tag a post.
There are 3 layers in the application, which are resolvers, services and repositories.
Resolversare responsible for handling GraphQL queries. This is where the correctness of the query is validated, and the format of the returned response is defined.Servicesare responsible for executing business logic. This is where, for example, we are checking whether the user can perform an action.Repositoriesare responsible for reading and writing data to the database.
- A class with the
Argsending (.args.tsfile suffix) denotes the arguments to the GraphQL query. - A class with the
Constraintsending (.constraints.tsfile suffix) denotes an object that encapsulates constant values used when validating an incoming payload during query or mutation. - A class with the
Descriptionending (.description.tsfile suffix) denotes an object that encapsulates GraphQL schema descriptions. - A class with the
Exceptionending (.exception.tsfile suffix) denotes an exception that can be thrown in an application. - A class with the
Inputending (.input.tsfile suffix) denotes the arguments to the GraphQL mutation. - A class with the
Modelending (.model.tsfile suffix) denotes a business model used in the application on which additional methods are available to perform business logic. - A class with the
Recordending (.record.tsfile suffix) denotes an entity stored in the database. - A class with the
Responseending (.response.tsfile suffix) denotes the response type of GraphQL query or mutation.
Various types of notes that are made during the development of the project.
To create a one-to-one relationship where one user can only have one profile, first create the users table.
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable(`users`, (tableBuilder: Knex.CreateTableBuilder): void => {
tableBuilder.increments(`id`)
tableBuilder.string(`email`, 255).unique().notNullable()
})
}This migration creates the users table that:
- Contains the
idcolumn which is automatically marked as the primary key. - Contains the
emailcolumn of type string that can be a maximum of 255 characters long, has aUNIQUE INDEXcreated, and cannot take aNULLvalue.
Now you need to create a profiles table.
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable(`profiles`, (tableBuilder: Knex.TableBuilder): void => {
tableBuilder.increments(`id`)
tableBuilder.string(`photo`).notNullable()
tableBuilder.integer(`user_id`).unique().references(`users.id`)
})
}This migration creates the profiles table that:
- Contains the
idcolumn which is automatically marked as the primary key. - Contains the
photocolumn which is an arbitrary column that is used to store urls to profile pictures. - Contains the
user_idcolumn which is a reference to theidcolumn inuserstable. This allows you to perform a query withJOINstatement. To ensure that a user can only have one profile, theuser_idcolumn has aUNIQUE INDEXthat will throw an error if we want to assign 2 profiles to one user.
To create a one-to-many relationship where one user can be the author of many posts, first create the users table.
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable(`users`, (tableBuilder: Knex.CreateTableBuilder): void => {
tableBuilder.increments(`id`)
tableBuilder.string(`email`, 255).unique().notNullable()
})
}This migration creates the users table that:
- Contains the
idcolumn which is automatically marked as the primary key. - Contains the
emailcolumn of type string that can be a maximum of 255 characters long, has aUNIQUE INDEXcreated, and cannot take aNULLvalue.
Now you need to create a posts table.
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable(`posts`, (tableBuilder: Knex.CreateTableBuilder): void => {
tableBuilder.increments(`id`)
tableBuilder.string(`title`).unique().notNullable()
tableBuilder.string(`content`).notNullable()
tableBuilder.integer(`user_id`).references(`users.id`)
})
}This migration creates the posts table that:
- Contains the
idcolumn which is automatically marked as the primary key. - Contains the
titlecolumn which is an arbitrary column that is used to store post title. It has aUNIQUE INDEXwhich assures that there cannot be 2 posts with the same title in the database. The column cannot takeNULLvalue due to.notNullable()constraint. - Contains the
contentcolumn which is an arbitrary column that is used to store post content. - Contains the
user_idcolumn which is a reference to theidcolumn inuserstable. This allows you to perform a query withJOINstatement. Important fact to notice here is that there is noUNIQUE INDEXconstraint which allows to create many post records that reference the sameuser_id.
To create a many-to-many relationship where one post can have many tags, and one tag can be assigned to many posts at the same time, first create the posts table.
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable(`posts`, (tableBuilder: Knex.CreateTableBuilder): void => {
tableBuilder.increments(`id`)
tableBuilder.string(`title`).unique().notNullable()
tableBuilder.string(`content`).notNullable()
})
}This migration creates the posts table that:
- Contains the
idcolumn which is automatically marked as the primary key. - Contains the
titlecolumn which is an arbitrary column that is used to store post title. It has aUNIQUE INDEXwhich assures that there cannot be 2 posts with the same title in the database. The column cannot takeNULLvalue due to.notNullable()constraint. - Contains the
contentcolumn which is an arbitrary column that is used to store post content.
Now you need to create a tags table.
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable(`tags`, (tableBuilder: Knex.TableBuilder): void => {
tableBuilder.increments(`id`)
tableBuilder.string(`name`).unique().notNullable()
})
}This migration creates the tags table that:
- Contains the
idcolumn which is automatically marked as the primary key. - Contains the
namecolumn which is an arbitrary column that is used to store tag name. It has aUNIQUE INDEXwhich assures that there cannot be 2 posts with the same title in the database. The column cannot takeNULLvalue due to.notNullable()constraint.
Once you have created both tables, it is time to create a relationship between them.
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable(`posts_tags`, (tableBuilder: Knex.TableBuilder): void => {
tableBuilder.increments().primary()
tableBuilder.integer(`post_id`).unsigned().references(`id`).inTable(`posts`)
tableBuilder.integer(`tag_id`).unsigned().references(`id`).inTable(`tags`)
})
}This migration creates the posts_tags table that:
- Contains the
post_idcolumn which references theidcolumn in thepoststable. - Contains the
tag_idcolumn which references theidcolumn in thetagstable.
This "proxy" table allows you to store the relationship between posts and tags in your application.
The N+1 problem is a situation when our application makes many queries to the database that look exactly the same. A typical example of this problem is an application where a user can create posts, and we provide an API where the application client can retrieve a list of posts along with the authors. The N+1 problem can appear when we try to fetch nested, related data (e.g. a list of posts with authors).
query Posts {
posts {
title
id
content
author {
email
id
}
}
}If we try to retrieve 20 posts, while they are all written by the same user, and the resolver responsible for author field is performing a SELECT statement based on author_id from post entity, we will run the same query 20 times (1 time for each post).
There are 2 potential ways of solving the N+1 problem. Both approaches have their pros and cons, so you should determine for yourself which approach is more appropriate for your particular case. To read more about particular implementation check out this blog post.
Dataloader is a class/function that is designed to fetch records in batches (i.e. using IN statements). You can use dataloader library.
- Pros
- Decreased CPU usage on the database server when the client application does not need data from nested relationship (we do not perform unnecessary
JOINoperation). - Decreased cost when the client application does not need data from nested relationship (we send less data over the wire, and cloud providers typically charge for network usage).
- Decreased response time when the client application does not need data from nested relationship (we do not perform unnecessary
JOINoperation).
- Decreased CPU usage on the database server when the client application does not need data from nested relationship (we do not perform unnecessary
- Cons
- Increased response when the client application needs data from nested relationship (we must run 2 separate database queries).
A JOIN statement is a database operation that allows you to retrieve related records. To read more about JOIN statements check out postgresqltutorial.
- Pros
- Decreased response time when the client application needs data from nested relationship (we are already running
JOINoperation in a single query).
- Decreased response time when the client application needs data from nested relationship (we are already running
- Cons
- Increased CPU usage on the database server when the client application does not need data from nested relationship (we are running unnecessary
JOINoperation). - Increased cost when the client application does not need data from nested relationship (we send more data over the wire, and cloud providers typically charge for network usage).
- Increased response time when the client application does not need data from nested relationship (we are running unnecessary
JOINoperation).
- Increased CPU usage on the database server when the client application does not need data from nested relationship (we are running unnecessary
Subscriptions allow you to listen to events that are happening in the application in real time. Usually, subscriptions in GraphQL are implemented with WebSockets. To learn more about subscriptions read this post describing how they work and how they are used in NestJS, or refer to the framework's official documentation.