Building a CRUD-backend with GraphQL, TypeScript and TypeGraphQL
Swayne
Posted on March 26, 2021
Intro
Hey, Swayne here. Over the next couple of months I will be writing some articles on graphQL, because I want to understand this better. Like always, I encourage you to tear me to shreds and I would like for you to correct/question every small detail (seriously). Thank you.
For this first article I just want develop a simple hello world app. I will be testing with GraphQL playground. Note: I will be writing this using typescript and type-graphql, but it should be the same besides the explicit type definitions (and awesome autofill😉). But of course let me know if you are used to JS, I will translate it for you.
What do you need?
The recipe to a good GraphQL-based backend
✅ Resolver
🎩. Schema
💻 Apollo-Server
I will go through the basics of a GraphQL-backend using TypeScript. I will also be using TypeGraphQL.
Basics of GraphQL
To send queries with GraphQL, you have to define your types first. It's like the schema of your API, so it tells which requests should return which types. Here is an example when getting a type Person:
type Person {
name: String!
age: Int!
}
You are telling graphQL what type it should expect when getting the name or age of a person. Note the exclamation point !
means the field cannot be null. You don't have to define this, it's completely optional, but improves your design and database structure.
Type-GraphQL classes
TypeGraphQL is a GraphQL framework for TypeScript, which makes working with queries and schemas easier. I like TypeGraphQL (TGQL), because I think the structure is simpler and the developer experience nicer. Let's look at the above type translated into TGQL using classes and decorators
@ObjectType()
class Person {
@Field()
name: String!
@Field()
age: Int!
}
You will notice that we have added @Field()
and @ObjectType
. These are called decorators. @Field
is used to declare what a field is, and @ObjectType
marks the class as a GraphQL type.
Resolver
There are two different types of resolvers, Mutations and Queries. Queries are read-only requests to get and view data from the GQL API. Mutations are resolvers where you create, update or delete data through the API, as the name indicates. Resolvers are functions, and in TGQL you (like in the Schema) have to make a class first.
@Resolver()
class UserResolver {
}
You also have to use the @Resolver()
decorator. Here is an example of a simple query:
import { Query, Resolver } from "type-graphql";
@Resolver()
export class HelloWorldResolver {
@Query(() => String)
hello() {
return "hi!";
}
}
As you can see you define a hello()
function and which returns a type string with the name hello()
and returns a string of "hi!".
We can now move on to an actual use-case.
CRUD-guide with a Database, TS, GraphQL and Type-GraphQL, TypeORM
We will be studying the follow technologies:
Tech-Stack
- GraphQL
- Type-GraphQL
- TypeORM
- SQL-lite
- TypeScript
The code for this tutorial is available on Github under the branch "server-CRUD".
Intialize the repo with Ben Awads command npx create-graphql-api graphql-example
and delete all of the code regarding PostgresSQL in ormconfig.json
You can also just clone this starter GitHub Repo I made.
Change the data in index.ts to:
(async () => {
const app = express();
const options = await getConnectionOptions(
process.env.NODE_ENV || "development"
);
await createConnection({ ...options, name: "default" });
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [HelloWorldResolver],
validate: true
}),
context: ({ req, res }) => ({ req, res })
});
apolloServer.applyMiddleware({ app, cors: false });
const port = process.env.PORT || 4000;
app.listen(port, () => {
console.log(`server started at http://localhost:${port}/graphql`);
});
})();
To start with, we are creating an app with express()
await createConnection();
createConnection() is from TypeORM, which establishes a connection to the SQL-lite database.
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [HelloWorldResolver],
validate: true
}),
context: ({ req, res }) => ({ req, res })
});
There are two important concepts in the above code, apolloServer
and buildSchema()
. ApolloServer is a sort of middle layer between your Server and Client. In our case we will be using it to define a schema-property, by calling the buildSchema-function from TypeGraphQL.
To build a schema, you need resolvers. Right now we are using a standard HelloWorldResolver, which we will look at soon. We are also using Apollo to get the context, making it possible to share a database connection between resolvers. Lastly, validate: true
forces TypeGraphQL to validate inputs and arguments based on the definitions of your decorators.
Let's look at the last few lines in index.ts
apolloServer.applyMiddleware({ app, cors: false });
Here we are applying the apolloServer as middleware and passing on our express-app, "connecting" those two.
Lastly, we go app.listen()
app.listen(port, () => {
console.log(`server started at http://localhost:${port}/graphql`);
});
})();
app.listen()
takes a port and starts the server on that given port!
Entities in TGQL
After some setup, we are ready!
There are many variations of a CRUD-app, so the difference of a note-taking app and blog-post-app is often just the column names! Point being, you can adjust this to your own needs. I will be making an app to save the scores of the pick-up basketball games I play🏀,
Let's look create a starter entity to define the general structure of our application:
import { Field, Int } from "type-graphql";
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
@ObjectType()
@Entity()
export class Game extends BaseEntity {
@Field(() => Int)
@PrimaryGeneratedColumn()
id: number;
@Field(() => Int)
@Column('int')
myTeamScore: number;
@Field(() => Int)
@Column()
opponentTeamScore: number;
@Column()
date: string;
}
This is a pretty simple Game
, where we save an id
, myTeamScore
, opponentTeamScore
and date
. We are making sure to give type-definition for each Column. Note, using a date
-type for the date-attribute would be better practice, but handling dates in Typescript is nearly an article on it's own😅 For now, we can treat dates as a string, but I will show you how to handle them using the Date-type another time. I promise🤝
We are using the @Field()
-decorator to declare the types of our field. Sometimes TGQL automatically infers them, but for numbers you have to declare the type explicitly.
On the line above the attributes, we are using two decorators @Column
and PrimaryGeneratedColumn()
. You need at least one PrimaryGeneratedColumn()
, so it's possible to uniquely identify each user. The rest are just standard Columns in a database table.
Type-ORM will automatically infer the types from the TypeScript-types, but you can also set them manually:
@Column('int')
myTeamScore: number;
You have to check what types your database-provider uses by looking it up in the docs📄
If you wanted to, you could also save a playerName
or teamName
as string, but that it for another tutorial😉
Let's write some resolvers to actually create, read, update and delete in the database! First, start the server by running yarn start
, as you can see in the package.JSON
:
"scripts": {
"start": "nodemon --exec ts-node src/index.ts",
"build": "tsc"
Creating a game
Create a new file called GameResolver.ts
in the resolvers folder please 🥺
The basic structure of a resolver is:
import { Mutation, Resolver } from "type-graphql";
@Resolver()
export class GameResolver extends BaseEntity {
@Mutation()
createGame() {
}
}
We use the @Mutation
-decorator to signify that we want to make a change. createGame()
is the name of the function.
You have to add it to your resolvers-array in the buildSchema-function from index.ts:
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [HelloWorldResolver, GameResolver]
}),
context: ({ req, res }) => ({ req, res })
});
I will be building the Resolver step-by-step and explaining as I go:
import { Arg, Int, Mutation, Resolver,} from "type-graphql";
@Resolver()
export class GameResolver {
@Mutation(() => Boolean)
createGame(
@Arg('myTeamScore', () => Int) myTeamScore: number,
) {
console.log(myTeamScore)
return true
}
}
On line 3, I set the return type for the resolver as a Boolean. This isn't really important right now, as I am just returning true
if it worked. I also log the score✏️
On line 5 I use the @Arg()
-decorator from TGQL decorator to pass in my arguments. Inside the decorator, I set the TGQL-type of the argument myTeamScore
to Int
and outside the parenthesis I set the TypeScript type. Note, that you have to import Int
from TGQL, since in GraphQL, the type number
can either be an Int
or a Float
, which is why you need to specify further.
Let's add the actual logic for inserting a Game into the database⚡️
@Resolver()
export class GameResolver {
@Mutation(() => Boolean)
async createGame(
@Arg('myTeamScore', () => Int) myTeamScore: number,
@Arg('opponentTeamScore', () => Int) opponentTeamScore: number,
@Arg('date', () => String) date: string,
) {
await Game.insert({myTeamScore, opponentTeamScore, date})
console.log(myTeamScore, date);
return true
}
}
On lines 5-7 I added more @Args()
based on my Entity in Game.ts. On line 9, we use the TypeORM insert method to add a Game
to the database.
Now, it's time to test our new Resolver.
GraphQL Playground
We will be testing these using GraphQL playground from Prisma. Go to "localhost:4000/graphQL" in your browser. In the GraphQL playground, you can write out different queries. To try out over resolver, we will write in the window:
mutation {
createGame(
myTeamScore: 21,
opponentTeamScore: 19,
date: "19-01-2020"
)
}
This is like calling any function from other programming languages. I add in my own sample data. As a developer, reality can be whatever you want, so (naturally) my team wins😇
Getting the games
We can create a Query for getting the movies.
@Query(() => [Game])
games() {
return Game.find()
}
We want to return an array of Game
-objects, and in the method body we use Game.find()
from typeORM to, well, find them😄
In the GraphQL Playground we can then write the query:
query {
games{
id,
myTeamScore,
opponentTeamScore,
date
}
}
This will get all of the games. The amazing thing about GraphQL (compared to REST atleast), is that you can choose what data to get. In example, you can remove the date-property from the above query if you don't need it. This is really efficient and especially useful for larger projects.
Update
Say that we want to update a game, we need to create a new resolver:
@Mutation(() => Boolean)
async updateGame(
@Arg('id', () => Int) id: number,
@Arg('myTeamScore', () => Int) myTeamScore: number,
@Arg('opponentTeamScore', () => Int) opponentTeamScore: number,
@Arg('date', () => String) date: string,
) {
await Game.update({id}, {myTeamScore, opponentTeamScore, date})
return true
}
The resolver above takes in 4 arguments:
- an
id
****to identify what post to delete - an updated
myTeamScore
,opponentTeamScore
anddate
.
You then call Game.update()
(also a function from TypeORM) which updates the database values. Lastly, I return true. We can now head over to the GraphQL Playgrpund:
mutation {
updateGame(
id: 1
myTeamScore: 19,
opponentTeamScore: 21,
date: "19-01-2020"
)
}
To update we make sure to pass in some sample updated values.
Delete
The last of the CRUD-operations, delete. To delete you just need an id to identify the post.
@Mutation(() => Boolean)
async deleteGame(
@Arg("id", () => Int) id: number
) {
await Game.delete({id})
return true
}
You can then call Game.delete()
and pass in the id
as an object
In the playground:
mutation {
deleteGame(id: 1)
}
I want to delete the first post, so I pass in the id
.
Conclusion
As you can see, GraphQL gives us a structured way of making operations on the server. Using Type-GraphQL and TypeORM we can set up our Entities and and any write mutator/query resolvers we can think of. The general process is:
1️⃣ Write your entities with types and decorators.
2️⃣ Decide what you want your resolver to return.
3️⃣ Pass in the args from your entity.
4️⃣ Make the needed operation in your resolver body.
And that's it! ✅
However, there are some ways to simplify our @Args()
. As you probably noticed, the Resolvers quickly get ugly the more Arguments we add. This project is pretty small, but imagine if we had more! The solution is to refactor the arguments into a separate input classes, which I will explain further in the article on Authtenthication, which is also worth reading!🙏
Feel free to leave any feedback either here or on my Twitter
Posted on March 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.