DEV Community

loading...
CodepediaOrg

CRUD API example with Express GraphQL

ama profile image Adrian Matei Originally published at codepedia.org ・9 min read

As you might recall from my previous post GraphQL Resources to help you get started, I have started to dig deeper into GraphQL. What better way to deepen one's knowledge than with a hands-on experience? So, in this blog post I will present the implementation of a GraphQL server API that provides CRUD operations. I chose the Javascript implementation of GraphQL, GraphQL-js 1 and set up a GraphQL server with Express Graphql2.

To make the scenario more realistic, the API developed in GraphQL acts as integration layer to the existing REST API supporting Bookmarks.dev.

You can find the source code for the examples in this post on

GitHub logo CodepediaOrg / graphql-express-crud-demo

Complete CRUD API written with graphql-express


Configure the demo project to test along

If you want to test along please follow the steps below:

Setup Bookmarks.dev localhost REST API

You need to first set up the localhost REST api of Bookmarks.dev. Checkout the project from Github and then follow the steps listed in the README file of the project.

Install and run the project graphql-expressjs-crud-demo

To run the actual GraphQL project you need to set it up as described in the README file of the project.

GraphiQL access

Once you are done with the setup, you can input your GraphQL queries with GraphiQL
by accessing the http://localhost:4000/graphql url in your favorite browser


The coming sections will present main elements of GraphQL with concrete examples and their implementation in GraphQL-js.

Schema

Every GraphQL service defines a set of types which completely describe the set of possible data you can query on that service. Then, when queries come in, they are validated and executed against that schema. Bellow you can find some of the most common types:

Object types and fields

The most basic components of a GraphQL schema are object types, which just represent a kind of object you can fetch
from your service, and what fields it has. In the GraphQL schema language, we might represent it like this:

type Bookmark {
  _id: ID!
  userId: ID!
  public: Boolean
  location: String!
  name: String!
  description: String
  tags: [String!]!
  likeCount: Int
  sourceCodeURL: String
}
Enter fullscreen mode Exit fullscreen mode

The language is pretty readable, but let's go over it so that we can have a shared vocabulary:

  • Bookmark is a GraphQL Object Type, meaning it's a type with some fields. Most of the types in your schema will be object types.
  • String, Boolean and Int are some of the built-in scalar types - these are types that resolve to a single scalar object, and can't have sub-selections in the query. We'll go over scalar types more later.
  • ID: The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache. The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be human‐readable.
  • String! means that the field is non-nullable, meaning that the GraphQL service promises to always give you a value when you query this field. In the type language, we'll represent those with an exclamation mark.
  • [String!]! represents an array of String objects. Since it is also non-nullable, you can always expect an array (with zero or more items) when you query the tags field. And since String! is also non-nullable, you can always expect every item of the array to be an String object.

The implementation in graphql-js looks something like the following:

const Bookmark = new GraphQLObjectType({
    name: "Bookmark",
    fields: {
        _id: {
            type: GraphQLID,
            description: "The id of the bookmark it's generated in MongoDb"
        },
        userId: {
            type:  GraphQLNonNull(GraphQLID),
            description: "The id of the user that created the bookmark"
        },
        public: {
            type: GraphQLBoolean,
            description: "Whether the bookmark is public or not"
        },
        location: {
            type:  GraphQLNonNull(GraphQLString),
            description: "Mostly the URL of the link"
        },
        name: {
            type: GraphQLNonNull(GraphQLString),
            description: "Title of the bookmark"
        },
        description: {
            type: GraphQLString,
            description: "Notes about the bookmark - supports Markdown"
        },
        tags: {
            type:  GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLString))),
            description: "Tags are highly used on Bookmarks.dev"
        },
        likeCount: {
            type: GraphQLInt,
            description: "Number of public likes"
        },
        sourceCodeURL: {
            type: GraphQLString,
            description: "Where you can find the source code related to bookmark"
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Arguments

Every field on a GraphQL object type can have zero or more arguments, for example the history field below:

type User {
 userId: ID!
 history(last: Int = 5): [Bookmark]
}
Enter fullscreen mode Exit fullscreen mode

All arguments are named. Unlike languages like JavaScript and Python where functions take a list of ordered arguments,
all arguments in GraphQL are passed by name specifically. In this case, the history field has one defined argument, last.

Arguments can be either required or optional. When an argument is optional, we can define a default value - if the last argument is not passed, it will be set to 5 by default.

The example above looks in GraphQL-js the following - focus on the history field of the User object part:

const User = new GraphQLObjectType({
    name: "User",
    fields: {
        userId: {
            type: GraphQLID,
            description: "user identifier - keycloak ID"
        },
        history: {
            type: new GraphQLList(Bookmark),
            description: "Bookmarks the user created, updated or clicked recently",
            args: {
                last: {
                    type: GraphQLInt,
                    defaultValue: 5,
                    description: "Fetches only *last* bookmarks from history "
                }
            },
            resolve: async (root, args, context) => {
                const userId = root.userId;
                const bearerToken = context.bearerToken;
                const last = args.last;
                const response = await bookmarksApiService.getBookmarksOfUserHistory(userId, bearerToken, last);

                return response.body;
            }
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Note the resolve method for now, but we'll talk about that later

Enumeration types

Also called Enums, enumeration types are a special kind of scalar that is restricted to a particular set of allowed values. This allows you to:

  1. Validate that any arguments of this type are one of the allowed values
  2. Communicate through the type system that a field will always be one of a finite set of values

Here's what an enum definition might look like in the GraphQL schema language:

enum OrderBy {
  MOST_LIKES
  LAST_CREATED
  MOST_USED
}
Enter fullscreen mode Exit fullscreen mode

This means that wherever we use the type OrderBy in our schema, we expect it to be exactly one of MOST_LIKES, LAST_CREATED, or MOST_USED.

In the Javascript GraphQL the definition of the enum looks like the following:

const BookmarkOrderByType = new GraphQLEnumType({
    name: 'OrderBy',
    values: {
        MOST_LIKES: {value: "MOST_LIKES"},
        LAST_CREATED: {value: "LAST_CREATED"},
        MOST_USED: {value: "MOST_USED"}
    }
});
Enter fullscreen mode Exit fullscreen mode

By using the value parameter we force the serialization to these values.

Queries - the R in CRUD

Queries are the bread and butter of GraphQL. You define the queries in the schema that your GraphQL provides under the root 'Query' object:

type Query {
    publicBookmarks: [Bookmark]
    user(userId: ID!): [User]
    bookmark(bookmarkId: ID!): [Bookmark]
}
Enter fullscreen mode Exit fullscreen mode

translated to the GraphQL javascript implementation:

const Query = new GraphQLObjectType({
    name: 'Query',
    fields: {
        publicBookmarks: {
            type: new GraphQLList(Bookmark),
            resolve: async (root, args, context, info) => {
                const response = await bookmarksApiService.getPublicBookmarks();
                return response.body;
            }
        },
        userFeedBookmarks: {
            type: new GraphQLList(Bookmark),
            resolve: async (root, args, context, info) => {
                const {userId, bearerToken} = context;
                const response = await bokmarksApiService.getBookmarksForFeed(userId, bearerToken);
                return response.body;
            }
        },
        user: {
            type: User,
            args: {
                userId: {type: GraphQLID}
            },
            resolve: async (root, args, context) => {
                const bearerToken = context.bearerToken;
                const {userId} = args;
                const response = await bookmarksApiService.getUserData(userId, bearerToken);

                return response.body;
            }
        },
        bookmark: {
            type: Bookmark,
            args: {
                bookmarkId: {type: GraphQLID}
            },
            resolve: async (root, args, context, info) => {
                const bearerToken = context.bearerToken;
                const {bookmarkId} = args;
                const response = await bookmarksApiService.getBookmarkById(userId, bearerToken, bookmarkId);

                return response.body;
            }
        }
    },
});
Enter fullscreen mode Exit fullscreen mode

Let's see now how a query would look on the client side, for example to receive data for the mock user provided by the bookmarks.dev setup:

{
 user(userId:"a7908cb5-3b37-4cc1-a751-42f674d870e1") {
    userId,
    profile {
      displayName
      imageUrl
    },
    bookmarks(orderBy:LAST_CREATED) {
      ...bookmarkFields
    },
    feed  {
      ...bookmarkFields
    },
    history {
      ...bookmarkFields
    }
  }
}

fragment bookmarkFields on Bookmark {
  _id
  name
  location
  tags
  sourceCodeURL
  likeCount
}
Enter fullscreen mode Exit fullscreen mode

Note the fragment construct - Fragments let you construct sets of fields, and then include them in queries where you need to,
to not have to repeat yourself.

The response should look something similar to the following:

{
  "data": {
    "user": {
      "userId": "a7908cb5-3b37-4cc1-a751-42f674d870e1",
      "profile": {
        "displayName": "Mock",
        "imageUrl": "https://gravatar.com/avatar/bc461041c4caf5493530db7a69d4bf83?s=340"
      },
      "bookmarks": [
        {
          "_id": "5fa8db1897519f34ae94f7e2",
          "name": "Build a CRUD functionality with GraphQL and ExpressJS",
          "location": "https://www.codepedia.org/ama/complete-example-crud-api-express-graphql",
          "tags": [
            "graphql",
            "expressjs",
            "graphql-express",
            "rest",
            "api-design"
          ],
          "sourceCodeURL": "https://github.com/CodepediaOrg/graphql-express-crud-demo",
          "likeCount": null
        },
        {
          "_id": "5e9d4a463b837e57e76de0ae",
          "name": "Getting started with www.bookmarks.dev",
          "location": "https://www.bookmarks.dev/howto",
          "tags": [
            "programming",
            "resource",
            "blog",
            "open-source"
          ],
          "sourceCodeURL": "https://github.com/CodepediaOrg/bookmarks",
          "likeCount": 0
        },
        {
          "_id": "5e9d4a463b837e57e76de0ad",
          "name": "Collection of public dev bookmarks, shared with from www.bookmarks.dev",
          "location": "https://github.com/CodepediaOrg/bookmarks#readme",
          "tags": [
            "programming",
            "resource",
            "blog",
            "open-source"
          ],
          "sourceCodeURL": "https://github.com/CodepediaOrg/bookmarks",
          "likeCount": 0
        },
        {
          "_id": "5e9d4a463b837e57e76de0ac",
          "name": "Bookmarks Manager for Devevelopers & Co",
          "location": "https://www.bookmarks.dev/",
          "tags": [
            "programming",
            "blog",
            "resources",
            "open-source"
          ],
          "sourceCodeURL": "https://github.com/CodepediaOrg/bookmarks.dev",
          "likeCount": 0
        },
        {
          "_id": "5e9d4a463b837e57e76de0ab",
          "name": "Share coding knowledge – CodepediaOrg",
          "location": "https://www.codepedia.org/",
          "tags": [
            "programming",
            "blog",
            "open-source"
          ],
          "sourceCodeURL": "",
          "likeCount": 0
        }
      ],
      "feed": [
        {
          "_id": "5fa8db1897519f34ae94f7e2",
          "name": "Build a CRUD functionality with GraphQL and ExpressJS",
          "location": "https://www.codepedia.org/ama/complete-tutorial-crud-graphql-express",
          "tags": [
            "graphql",
            "expressjs",
            "graphql-express",
            "rest",
            "api-design"
          ],
          "sourceCodeURL": "https://github.com/CodepediaOrg/graphql-express-crud-demo",
          "likeCount": null
        },
        {
          "_id": "5f93b3a51e55b52d7b5d73bd",
          "name": "Issues · BookmarksDev/bookmarks.dev · GitHub",
          "location": "https://github.com/BookmarksDev/bookmarks.dev/issues",
          "tags": [
            "bookmarksdev"
          ],
          "sourceCodeURL": "",
          "likeCount": 0
        }
      ],
      "history": [
        {
          "_id": "5f93b3a51e55b52d7b5d73bd",
          "name": "Issues · BookmarksDev/bookmarks.dev · GitHub",
          "location": "https://github.com/BookmarksDev/bookmarks.dev/issues",
          "tags": [
            "bookmarksdev"
          ],
          "sourceCodeURL": "",
          "likeCount": 0
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Resolvers

In the Query section before you might have noticed the resolve method. These are so called Resolvers in the GraphQL terminology. If the schema defines the structure of the GraphQL API, then the resolvers implement the API and
determine the server's behaviour.

Each field in a GraphQL schema is backed by a resolver

" In its most basic form, a GraphQL server will have one resolver function per field in its schema. Each resolver knows how to fetch the data for its field. Since a GraphQL query at its essence is just a collection of fields, all a GraphQL server actually needs to do in order to gather the requested data is invoke all the resolver functions for the fields specified in the query. (This is also why GraphQL often is compared to RPC-style systems, as it essentially is a language for invoking remote functions.)"3

Anatomy of the resolver

Let's see the code snippet again for the bookmark query:

        bookmark: {
            type: Bookmark,
            args: {
                bookmarkId: {type: GraphQLID}
            },
            resolve: async (root, args, context, info) => {
                const bearerToken = context.bearerToken;
                const {bookmarkId} = args;
                const response = await bookmarksApiService.getBookmarkById(userId, bearerToken, bookmarkId);

                return response.body;
            }
        }
Enter fullscreen mode Exit fullscreen mode

Notice the parameters of the resolve function. They have the following meaning:

"

  1. root (also sometimes called parent): Remember how we said all a GraphQL server needs to do to resolve a query is calling the resolvers of the query’s fields? Well, it’s doing so breadth-first (level-by-level) and the root argument in each resolver call is simply the result of the previous call (initial value is null if not otherwise specified).
  2. args: This argument carries the parameters for the query, in this case the id of the User to be fetched.
  3. context: An object that gets passed through the resolver chain that each resolver can write to and read from (basically a means for resolvers to communicate and share information).
  4. info: An AST representation of the query or mutation. You can read more about the details Demystifying the info Argument in GraphQL Resolvers. " 3

Set parameter into the resolver's context in express middleware

You can also set parameters into the req object of the Express middleware and they will be available in the
context parameter in resolvers, as this is the cases for bearerToken from the previous example - const bearerToken = context.bearerToken;

const app = express();

const setAccessTokenMiddleware = async (req, res, next) => {
  const accessToken = await accessTokenService.getKeycloakAccessToken();
  req.bearerToken = 'Bearer ' + accessToken;

  const decoded = jwt.decode(accessToken);
  const userId = decoded.sub;
  req.userId = userId;
  next();
}

app.use(setAccessTokenMiddleware);
Enter fullscreen mode Exit fullscreen mode

The bearerToken is set into the context via the Express Middleware

Mutations - the CUD in CRUD

If queries are used for fetching data from the GraphQL server, then mutations are to modify the data on the GraphQL server.

"In REST, any request might end up causing some side-effects on the server, but by convention it's suggested that one doesn't use GET requests to modify data. GraphQL is similar - technically any query could be implemented to cause a data write. However, it's useful to establish a convention that any operations that cause writes should be sent explicitly via a mutation.

Just like in queries, if the mutation field returns an object type, you can ask for nested fields. This can be useful for fetching the new state of an object after an update. "4

Let's see what mutations are available for the demo project:

type Mutation {
    createBookmark(input: BookmarkInput!): Bookmark
    updateBookmark(bookmarkId: ID!, input: BookmarkInput!): Bookmark
    deleteBookmark(bookmarkId: ID!): Bookmark
}
Enter fullscreen mode Exit fullscreen mode

and the implementation in GraphQL-js is the following:

const Mutation = new GraphQLObjectType({
    name: 'Mutation',
    fields: {
        createBookmark: {
            type: Bookmark,
            args: {
                input: {type: BookmarkInput}
            },
            resolve: async (root, args, context) => {
                const { input } = args;

                const {userId, bearerToken} = context;
                const bookmark = await bookmarksApiService.createBookmark(bearerToken, userId, input);

                return bookmark;
            }
        },
        updateBookmark: {
            type: Bookmark,
            args: {
                bookmarkId: {type: GraphQLID},
                input: {type: BookmarkInput}
            },
            resolve: async (root, args, context) => {
                const { input, bookmarkId } = args;

                const {userId, bearerToken} = context;
                const bookmark = await bookmarksApiService.updateBookmark(bearerToken, userId, bookmarkId, input);

                return bookmark;
            }
        },
        deleteBookmark: {
            description: "Given its ID a bookmark can be deleted. Either by the one that created it or an Admin",
            type: Bookmark,
            args: {
                bookmarkId: {type: GraphQLID}
            },
            resolve: async (root, args, context) => {
                const bookmarkId = args.bookmarkId;
                const {userId, bearerToken} = context;
                const deletedBookmark = await bookmarksApiService.deleteBookmarkId(bearerToken, userId, bookmarkId);
                return deletedBookmark;
            }
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post you've learned a bit of theory about GraphQL's main elements accompanied by examples with their corresponding
implementation in GraphQL-JS. I really start to like GraphQL and I say it one more time - the best learning experience is a hands-on one.


  1. https://github.com/graphql/graphql-js 

  2. https://github.com/graphql/express-graphql 

  3. https://www.prisma.io/blog/graphql-server-basics-the-schema-ac5e2950214e 

  4. https://graphql.org/learn/queries/#mutations 

Discussion (0)

pic
Editor guide