DEV Community

loading...
Cover image for Building a CRUD-backend with GraphQL, TypeScript and TypeGraphQL

Building a CRUD-backend with GraphQL, TypeScript and TypeGraphQL

lastnameswayne profile image Swayne ・8 min read

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!
}
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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 {
}
Enter fullscreen mode Exit fullscreen mode

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!";
  }
}
Enter fullscreen mode Exit fullscreen mode

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`);
  });
})();
Enter fullscreen mode Exit fullscreen mode

To start with, we are creating an app with express()

    await createConnection();
Enter fullscreen mode Exit fullscreen mode

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 })
  });
Enter fullscreen mode Exit fullscreen mode

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.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/70a0595d-3763-4eef-90e1-b6e6fca7a0c0/Untitled.png

Let's look at the last few lines in index.ts

apolloServer.applyMiddleware({ app, cors: false });
Enter fullscreen mode Exit fullscreen mode

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`);
  });
})();
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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() {

    }
}
Enter fullscreen mode Exit fullscreen mode

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 })
  });
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
    )
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode

The resolver above takes in 4 arguments:

  • an id ****to identify what post to delete
  • an updated myTeamScore, opponentTeamScore and date.

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"
    )
}
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode

You can then call Game.delete() and pass in the id as an object

In the playground:

mutation {
  deleteGame(id: 1)
}
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)

Forem Open with the Forem app