Getting Started

In this tutorial, we will lead you through the process of creating a simple GraphQL backend application.

We'll use GQLoom with your favorite schema builder to define the GraphQL Resolver and Schema to build a simple cattery application that can query the cats in the cattery and can add new cats to the cattery.

You are going to use the following technologies:

  • Node.js:as an engine and runtime for our server;
  • TypeScript:a strongly typed programming language that builds on JavaScript giving you better tooling at any scale;
  • GraphQL.js:a reference implementation of GraphQL for JavaScript;
  • graphql-yoga:the easiest way to build an HTTP GraphQL server;
  • GQLoom:define GraphQL Schema and resolvers more simply and efficiently.

You can use a familiar Schema Builder such as Valibot, Zod, or even just use GraphQL.js.

Prerequisites

Before you start, make sure you have the following software installed:

This tutorial assumes that you have a basic knowledge of TypeScript, Node.js, GraphQL, and some knowledge of Valibot or Zod. If you are a beginner, we suggest you learn these basics first.

Initialize the project

First, we need to create a new Node.js project.

Open your command line and run the following command:

mkdir cattery cd cattery npm init -y

In the above command: we created a new directory named cattery and entered it. Then, we initialize a new Node.js project using the npm init -y command and automatically generate a default package.json file.

Install dependencies

Next, we need to install some necessary dependencies.

npm
yarn
pnpm
bun
npm install -D typescript @types/node tsx

In this step, we installed TypeScript, the type definition for Node.js, and tsx. tsx is a tool for running TypeScript in Node.js.

npm
yarn
pnpm
bun
npm install graphql graphql-yoga

We also installed graphql and graphql-yoga to help us run the GraphQL service.

Choose a Schema Builder

valibot
zod
graphql.js
npm
yarn
pnpm
bun
npm install @gqloom/core valibot @gqloom/valibot

Now, create a new TypeScript profile using the following command:

npx tsc --init

Starting the project

First, we add a dev script to package.json to start our application:

package.json
{ // ... "scripts": { "dev": "tsx watch src/index.ts" } // ... }

Then, we create an src/index.ts file and add the following code:

valibot
zod
graphql.js
src/index.ts
import { weave, resolver, query } from "@gqloom/valibot" import * as v from "valibot" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" const HelloResolver = resolver({ hello: query(v.string(), () => "Hello, World"), }) export const schema = weave(HelloResolver) const yoga = createYoga({ schema }) createServer(yoga).listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") })

In the code above: we define our GraphQL Resolver using resolver and query, we weave CatResolver into a GraphQL Schema with the weave function, and we start our GraphQL service with graphql-yoga.

Now, you can run the following command to start your application:

npm
yarn
pnpm
bun
npm run dev

You should see output similar to the following:

Server is running on http://localhost:4000/graphql

You can open your browser and visit http://localhost:4000/graphql and you will see a GraphQL playground where you can test your GraphQL queries:

For example, when we input:

query { hello }

You should see the following output:

{ "data": { "hello": "Hello, World" } }

Writing Code

Now that you've successfully started your GraphQL service, let's try to construct slightly more complex functionality.

Defining the Cat type

Next, we define a Cat type with a name field and a birthDate field.

valibot
zod
graphql.js
src/index.ts
import * as v from "valibot" const Cat = v.object({ __typename: v.nullish(v.literal("Cat")), name: v.string(), birthDate: v.string(), }) interface ICat extends v.InferOutput<typeof Cat> {}

In the above code, we have used v.object to define the Cat type: It has a __typename field with a value of “Cat”, which will be used as the name of this object when weaving the GraphQL Schema, and we also set __typename to nullish so that we don't have to carry the __typename property for each instance of Cat object at runtime This avoids having to carry the __typename property for each instance of Cat at runtime; There is also a name field and a birthDate field, both of which are of type string.

Finally, we easily get the output type of Cat using v.InferOutput and name it ICat.

GQLoom will weave the Cat type we just defined into a GraphQL Schema:

type Cat { name: String! birthDate: String! }

Managing data

To manage our data, we simply use a Map object to store Cat instances:

src/index.ts
const catMap = new Map<string, ICat>([ ["Tom", { name: "Tom", birthDate: "2023-03-03" }], ])
TIP

In this tutorial, we are storing the data directly using JavaScript's Map object for the purpose of code simplicity. This will store the data in memory and when the server restarts, the data will be lost.

In practice, you may need to use a more robust data persistence storage solution, such as a database.

Defining the query operation

The query operation is the entry point to the GraphQL Schema that allows clients to query data.

Now, let's go back to the beginning with CatResolver and add a query operation to it called cats which returns all Cat instances:

valibot
zod
graphql.js
src/index.ts
import { weave, resolver, query } from "@gqloom/valibot" import * as v from "valibot" const CatResolver = resolver({ cats: query(v.array(Cat), () => Array.from(catMap.values())), }) const HelloResolver = resolver({ hello: query(v.string(), () => "Hello, World"), }) export const schema = weave(HelloResolver, CatResolver)

In the code above, we use the resolver function to define CatResolver and add a query operation named cats that returns all Cat instances. The query function takes two arguments:

  • The first argument is the output type of cats, which you can pass directly into the valibot schema, in this case v.array(Cat); the second argument is a parser function, which returns all instances of Cat.
  • The second argument is a parser function where we define the specific resolving logic for cats, where we use the Array.from function to convert catMap to an array and use it as the return value for cats.

In addition, we also weave together CatResolver and HelloResolver using the weave function to create the final GraphQL Schema.

Let's try to access the cats operation in the playground:

query cats { cats { name birthDate } }

You should see the following output:

{ "data": { "cats": [ { "name": "Tom", "birthDate": "2023-03-03" } ] } }

Defining input

Next, we define a query operation called cat that takes a name argument and returns the instance of Cat matching that name:

valibot
zod
graphql.js
src/index.ts
import { resolver, query } from "@gqloom/valibot" import * as v from "valibot" const CatResolver = resolver({ cats: query(v.array(Cat), () => Array.from(catMap.values())), cat: query(v.nullish(Cat), { input: { name: v.string(), }, resolve: ({ name }) => catMap.get(name), }), hello: query(v.string(), () => "Hello, World"), })

In the code above, we add a query operation called cat to CatResolver.

Similar to cats, the query function used to build cat takes as its first argument the nullish type of Cat, which means that the return value of the cat operation can be of type null or Cat.

In the second argument, we still pass a parser function, but this time we pass an additional input parameter that defines the input type for the cat operation. The input parameter is an object containing an attribute named name, which is of type string. When the cat operation is accessed, GQLoom internally calls the parse function of valibot to ensure that the value of the name parameter matches the string type.

In the resolve function, we get the value of the name parameter from the first parameter of the resolve function, TypeScript infers the type of the name parameter to be string, and we use the catMap.get method to get the instance of Cat that corresponds to the name, which is used as the return value of the cat operation. as the return value of the cat operation.

Let's try to access the cat operation in the playground:

query cat { cat(name: "Tom") { name birthDate } }

You should see the following output:

{ "data": { "cat": { "name": "Tom", "birthDate": "2023-03-03" } } }

Defining the mutation operation

The mutation operation is used to modify data, such as creating, updating, or deleting data.

Now, let's add a mutation operation named createCat to CatResolver that takes a name parameter and returns a Cat instance.

valibot
zod
graphql.js
src/index.ts
import { resolver, query, mutation } from "@gqloom/valibot" import * as v from "valibot" const CatResolver = resolver({ cats: query(v.array(Cat), () => Array.from(catMap.values())), cat: query(v.nullish(Cat), { input: { name: v.string(), }, resolve: ({ name }) => catMap.get(name), }), createCat: mutation(Cat, { input: { name: v.string(), birthDate: v.string(), }, resolve: ({ name, birthDate }) => { const cat = { name, birthDate } catMap.set(name, cat) return cat }, }), hello: query(v.string(), () => "Hello, World"), })

In the code above, we have added a mutation operation named createCat to CatResolver.

The input to the mutation function is the same as the query function.

Here, the createCat operation has a return type of Cat and accepts as input two parameters, name and birthDate, both of which are of type string.

In the parser function, we can easily get the values of the name and birthDate parameters from the first parameter, TypeScript will infer their types for us, and then we create a new Cat instance, add it to the catMap, and finally return the Cat instance.

Let's try to create a new Cat instance in the playground:

mutation createCat { createCat(name: "Nala", birthDate: "2020-01-01") { name birthDate } }

You should see results similar to the following:

{ "data": { "createCat": { "name": "Nala", "birthDate": "2020-01-01" } } }

Let's use the cats query to get all Cat instances:

query cats { cats { name birthDate } }

You should see results similar to the following:

{ "data": { "cats": [ { "name": "Tom", "birthDate": "2023-03-03" }, { "name": "Nala", "birthDate": "2020-01-01" } ] } }

Defining fields

Now, let's try to define an age field for the Cat type.

The age field is not stored in the Cat instance, but is calculated on each query.

valibot
zod
graphql.js
src/index.ts
import { resolver, query, mutation, field } from "@gqloom/valibot" import * as v from "valibot" const CatResolver = resolver.of(Cat, { age: field(v.pipe(v.number(), v.integer()), (cat) => { const birthDate = new Date(cat.birthDate) return new Date().getFullYear() - birthDate.getFullYear() }), cats: query(v.array(Cat), () => Array.from(catMap.values())), cat: query(v.nullish(Cat), { input: { name: v.string(), }, resolve: ({ name }) => catMap.get(name), }), // ... })

In the code above, we have added a field named age to CatResolver.

Notice that we used the resolver.of function instead of resolver. The first argument to the resolver.of function is an object Schema, in this case Cat, which will be used as the source type for CatResolver; In the second argument, we still pass in query, mutation, and field to define CatResolver.

In the field named age, we use v.pipe(v.number(), v.integer()) to define the type of age, and GQLoom will weave the age field into a GraphQL Int type. Note that GQLoom does not execute a parse step on the output of a parser function by default, because the results produced inside a parser function are usually controllable and follow the TypeScript-derived type.

In the resolve function, we easily get the value of the cat instance from the first parameter, TypeScript will infer its type for us, then we convert the cat instance's birthDate field to a Date instance and calculate the difference between the current year and the year of the birthDate, and finally return the difference as the cat's age.

Let's try to visit the cat operation in the playground:

query cat { cat(name: "Tom") { name birthDate age } }

You should see the following output:

{ "data": { "cat": { "name": "Tom", "birthDate": "2023-03-03", "age": 1 } } }

Adding input to a field

We can add an input object to a field, which will be used as the input parameter for that field.

valibot
zod
graphql.js
src/index.ts
import { resolver, query, mutation, field } from "@gqloom/valibot" import * as v from "valibot" const CatResolver = resolver.of(Cat, { age: field(v.pipe(v.number(), v.integer()), { input: { year: v.nullish(v.pipe(v.number(), v.integer()), () => new Date().getFullYear() ), }, resolve: (cat, { year }) => { const birthDate = new Date(cat.birthDate) return year - birthDate.getFullYear() }, }), cats: query(v.array(Cat), () => Array.from(catMap.values())), cat: query(v.nullish(Cat), { input: { name: v.string(), }, resolve: ({ name }) => catMap.get(name), }), // ... })

In the above code, we have added an input object to the age field, which contains a field named year of type Int, which uses the current year as the default value if no year input is provided. In the field parse function, we can easily get the value of year from the second argument.

Let's try to access the cat operation in the playground:

query cat { cat(name: "Tom") { name birthDate age(year: 2026) } }

You should see the following output:

{ "data": { "cat": { "name": "Tom", "birthDate": "2023-03-03", "age": 3 } } }

Summary

Very well, we have written a simple GraphQL APP that contains a CatResolver. In the example we just learned:

  • Define methods for query, mutation in resolver;
  • Defining objects and fields using valibot, zod or graphql.js;
  • Defining parsing functions and input parameters in query, mutation, field;
  • Weave CatResolver and HelloResolver into a GraphQL Schema using the weave function and launch our GraphQL APP using graphql-yoga.

Next step

  • Check out the core concepts of GQLoom: silk, resolver, weave;
  • Understanding common features: context, DataLoader, Middleware
  • See Valibot Integration for documentation on how to use Valibot to build more complex GraphQL objects and advanced types such as Union, Interface, and Enum.
  • See Zod Integration documentation to learn how to use Zod to build more complex GraphQL objects and advanced types like Union, Interface, and Enum.