Guide
To quickly get started with GQLoom, we will build a simple GraphQL backend application together.
We will build a cattery application and provide a GraphQL API to the outside. This application will include some simple functions:
- Cat basic information management: Enter the basic information of cats, including name, birthday, etc., update, delete and query cats;
- User (cat owner) registration management: Enter user information, a simple login function, and view one's own or other users' cats;
We will use the following technologies:
- TypeScript: As our development language;
- Node.js: As the runtime of our application;
- graphql.js: The JavaScript implementation of GraphQL;
- GraphQL Yoga: A comprehensive GraphQL HTTP adapter;
- Drizzle ORM: A fast and type-safe ORM that helps us operate the database;
- Valibot or Zod: Used to define and validate inputs;
GQLoom
: Allows us to define GraphQL Schema comfortably and efficiently and write resolvers;
Prerequisites
We only need to install Node.js version 20 or higher to run our application.
Create the Application
Project Structure
Our application will have the following structure:
- src
- contexts
- providers
- resolvers
- schema
- index.ts
- drizzle.config.ts
- package.json
- tsconfig.json
Among them, the functions of each folder or file under the src
directory are as follows:
contexts
: Store contexts, such as the current user;providers
: Store functions that need to interact with external services, such as database connections and Redis connections;resolvers
: Store GraphQL resolvers;schema
: Store the schema, mainly the database table structure;index.ts
: Used to run the GraphQL application in the form of an HTTP service;
INFO
GQLoom has no requirements for the project's file structure. Here is just for reference. In practice, you can organize the files according to your needs and preferences.
Initialize the Project
First, let's create a new folder and initialize the project:
mkdir cattery
cd ./cattery
npm init -y
mkdir cattery
cd ./cattery
pnpm init
mkdir cattery
cd ./cattery
yarn init -y
Then, we will install some necessary dependencies to run a TypeScript application in Node.js:
npm i -D typescript @types/node tsx
npx tsc --init
pnpm add -D typescript @types/node tsx
pnpm exec tsc --init
yarn add -D typescript @types/node tsx
yarn dlx -q -p typescript tsc --init
Next, we will install GQLoom and related dependencies. We can choose Valibot or Zod to define and validate inputs:
# use Valibot
npm i graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
# use Zod
npm i graphql graphql-yoga @gqloom/core zod @gqloom/zod
# use Valibot
pnpm add graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
# use Zod
pnpm add graphql graphql-yoga @gqloom/core zod @gqloom/zod
# use Valibot
yarn add graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
# use Zod
yarn add graphql graphql-yoga @gqloom/core zod @gqloom/zod
Hello World
Let's write our first resolver,we can choose to use Valibot
or Zod
:
import { query, resolver } from "@gqloom/core"
import * as v from "valibot"
const helloResolver = resolver({
hello: query(v.string())
.input({ name: v.nullish(v.string(), "World") })
.resolve(({ name }) => `Hello ${name}!`),
})
export const resolvers = [helloResolver]
We need to weave this resolver into a GraphQL Schema and run it as an HTTP server:
import { createServer } from "node:http"
import { weave } from "@gqloom/core"
import { ValibotWeaver } from "@gqloom/valibot"
import { createYoga } from "graphql-yoga"
import { resolvers } from "src/resolvers"
const schema = weave(ValibotWeaver, ...resolvers)
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
Great, we have already created a simple GraphQL application. Next, let's try to run this application. Add the dev
script to the package.json
:
{
"scripts": {
"dev": "tsx watch src/index.ts"
}
}
Now let's run it:
npm run dev
pnpm dev
yarn dev
Open http://localhost:4000/graphql in the browser and you can see the GraphQL playground. Let's try to send a GraphQL query. Enter the following in the playground:
{
hello(name: "GQLoom")
}
Click the query button, and you can see the result:
{
"data": {
"hello": "Hello GQLoom!"
}
}
So far, we have created the simplest GraphQL application.
Next, we will use Drizzle ORM to interact with the database and add complete functions.
Initialize the Database and Tables
First, let's install Drizzle ORM. We will use it to operate the SQLite database.
npm i @gqloom/drizzle drizzle-orm @libsql/client dotenv
npm i -D drizzle-kit
pnpm add @gqloom/drizzle drizzle-orm @libsql/client dotenv
pnpm add -D drizzle-kit
yarn add @gqloom/drizzle drizzle-orm @libsql/client dotenv
yarn add -D drizzle-kit
Define Database Tables
Next, define the database tables in the src/schema/index.ts
file. We will define two tables, users
and cats
, and establish the relationship between them:
import { drizzleSilk } from "@gqloom/drizzle"
import { relations } from "drizzle-orm"
import * as t from "drizzle-orm/sqlite-core"
export const users = drizzleSilk(
t.sqliteTable("users", {
id: t.int().primaryKey({ autoIncrement: true }),
name: t.text().notNull(),
phone: t.text().notNull().unique(),
})
)
export const usersRelations = relations(users, ({ many }) => ({
cats: many(cats),
}))
export const cats = drizzleSilk(
t.sqliteTable("cats", {
id: t.integer().primaryKey({ autoIncrement: true }),
name: t.text().notNull(),
birthday: t.integer({ mode: "timestamp" }).notNull(),
ownerId: t
.integer()
.notNull()
.references(() => users.id),
})
)
export const catsRelations = relations(cats, ({ one }) => ({
owner: one(users, {
fields: [cats.ownerId],
references: [users.id],
}),
}))
Initialize the Database
We need to create a configuration file:
import "dotenv/config"
import { defineConfig } from "drizzle-kit"
export default defineConfig({
out: "./drizzle",
schema: "./src/schema/index.ts",
dialect: "sqlite",
dbCredentials: {
url: process.env.DB_FILE_NAME ?? "file:local.db",
},
})
Then we run the drizzle-kit push
command to create the defined tables in the database:
npx drizzle-kit push
Use the Database
To use the database in the application, we need to create a database instance:
import { drizzle } from "drizzle-orm/libsql"
import * as schema from "src/schema"
export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", {
schema,
})
Resolvers
Now, we can use the user service in the resolver. We will create a user resolver and add the following operations:
usersByName
: Find users by nameuserByPhone
: Find users by phone numbercreateUser
: Create a user
After completing the user resolver, we also need to add it to the resolvers
in the src/resolvers/index.ts
file:
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import { db } from "src/providers"
import { users } from "src/schema"
import * as v from "valibot"
export const userResolver = resolver.of(users, {
usersByName: query(users.$list())
.input({ name: v.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: v.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: v.object({
name: v.string(),
phone: v.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
import { query, resolver } from "@gqloom/core"
import { userResolver } from "src/resolvers/user"
import * as v from "valibot"
const helloResolver = resolver({
hello: query(v.string())
.input({ name: v.nullish(v.string(), "World") })
.resolve(({ name }) => `Hello ${name}!`),
})
export const resolvers = [helloResolver, userResolver]
Great, now let's try it in the playground:
mutation {
createUser(data: {name: "Bob", phone: "001"}) {
id
name
phone
}
}
{
"data": {
"createUser": {
"id": 1,
"name": "Bob",
"phone": "001"
}
}
}
Let's continue to try to retrieve the user we just created:
{
usersByName(name: "Bob") {
id
name
phone
}
}
{
"data": {
"usersByName": [
{
"id": 1,
"name": "Bob",
"phone": "001"
}
]
}
}
Current User Context
First, let's add the asyncContextProvider
middleware to enable asynchronous context:
import { createServer } from "node:http"
import { weave } from "@gqloom/core"
import { asyncContextProvider } from "@gqloom/core/context"
import { ValibotWeaver } from "@gqloom/valibot"
import { createYoga } from "graphql-yoga"
import { resolvers } from "src/resolvers"
const schema = weave(asyncContextProvider, ValibotWeaver, ...resolvers)
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
Next, let's try to add a simple login function and add a query operation to the user resolver:
mine
: Return the current user information
To implement this query, we first need to have a login function. Let's write a simple one:
import { createMemoization, useContext } from "@gqloom/core/context"
import { eq } from "drizzle-orm"
import { GraphQLError } from "graphql"
import type { YogaInitialContext } from "graphql-yoga"
import { db } from "src/providers"
import { users } from "src/schema"
export const useCurrentUser = createMemoization(async () => {
const phone =
useContext<YogaInitialContext>().request.headers.get("authorization")
if (phone == null) throw new GraphQLError("Unauthorized")
const user = await db.query.users.findFirst({ where: eq(users.phone, phone) })
if (user == null) throw new GraphQLError("Unauthorized")
return user
})
In the above code, we created a context function for getting the current user, which will return the information of the current user. We use createMemoization()
to memoize this function, which ensures that this function is only executed once within the same request to avoid unnecessary database queries.
We used useContext()
to get the context provided by Yoga, and obtained the user's phone number from the request header, and found the user according to the phone number. If the user does not exist, a GraphQLError
will be thrown.
WARNING
As you can see, this login function is very simple and is only used for demonstration purposes, and it does not guarantee security at all. In practice, it is usually recommended to use solutions such as session
or jwt
.
Now, we add the new query operation in the resolver:
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import { useCurrentUser } from "src/contexts"
import { db } from "src/providers"
import { users } from "src/schema"
import * as v from "valibot"
export const userResolver = resolver.of(users, {
mine: query(users).resolve(() => useCurrentUser()),
usersByName: query(users.$list())
.input({ name: v.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: v.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: v.object({
name: v.string(),
phone: v.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
If we directly call this new query in the playground, the application will give us an unauthorized error:
{
mine {
id
name
phone
}
}
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"mine"
]
}
],
"data": null
}
Open the Headers
at the bottom of the playground and add the authorization
field to the request header. Here we use the phone number of Bob
created in the previous step, so we are logged in as Bob
:
{
"authorization": "001"
}
{
mine {
id
name
phone
}
}
{
"data": {
"mine": {
"id": 1,
"name": "Bob",
"phone": "001"
}
}
}
Resolver Factory
Next, we will add the business logic related to cats.
We use the resolver factory to quickly create interfaces:
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import { db } from "src/providers"
import { cats } from "src/schema"
import * as v from "valibot"
const catResolverFactory = drizzleResolverFactory(db, cats)
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(v.pipe(v.number()))
.derivedFrom("birthday")
.input({
currentYear: v.nullish(v.pipe(v.number(), v.integer()), () =>
new Date().getFullYear()
),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
})
import { query, resolver } from "@gqloom/core"
import { catResolver } from "src/resolvers/cat"
import { userResolver } from "src/resolvers/user"
import * as v from "valibot"
const helloResolver = resolver({
hello: query(v.string())
.input({ name: v.nullish(v.string(), "World") })
.resolve(({ name }) => `Hello ${name}!`),
})
export const resolvers = [helloResolver, userResolver, catResolver]
In the above code, we used drizzleResolverFactory()
to create catResolverFactory
for quickly building resolvers.
We added a query that uses catResolverFactory
to select data and named it cats
. This query will provide full query operations on the cats
table. In addition, we also added an additional age
field for cats to get the age of the cats.
Next, let's try to add a createCat
mutation. We want only logged-in users to access this interface, and the created cats will belong to the current user:
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import { useCurrentUser } from "src/contexts"
import { db } from "src/providers"
import { cats } from "src/schema"
import * as v from "valibot"
const catResolverFactory = drizzleResolverFactory(db, cats)
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(v.pipe(v.number()))
.derivedFrom("birthday")
.input({
currentYear: v.nullish(v.pipe(v.number(), v.integer()), () =>
new Date().getFullYear()
),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
createCats: catResolverFactory.insertArrayMutation().input(
v.pipeAsync(
v.objectAsync({
values: v.arrayAsync(
v.pipeAsync(
v.object({
name: v.string(),
birthday: v.pipe(
v.string(),
v.transform((x) => new Date(x))
),
}),
v.transformAsync(async ({ name, birthday }) => ({
name,
birthday,
ownerId: (await useCurrentUser()).id,
}))
)
),
})
)
),
})
In the above code, we used catResolverFactory
to create a mutation that adds more data to the cats
table, and we overwrote the input of this mutation. When validating the input, we used useCurrentUser()
to get the ID of the currently logged-in user and pass it as the value of ownerId
to the cats
table.
Now let's try to add a few cats in the playground:
mutation {
createCats(values: [{ name: "Mittens", birthday: "2021-01-01" }, { name: "Fluffy", birthday: "2022-02-02" }]) {
id
name
age
}
}
{
"authorization": "001"
}
{
"data": {
"createCats": [
{
"id": 1,
"name": "Mittens",
"age": 4
},
{
"id": 2,
"name": "Fluffy",
"age": 3
}
]
}
}
Let's use the cats
query to confirm the data in the database again:
{
cats {
id
name
age
}
}
{
"data": {
"cats": [
{
"id": 1,
"name": "Mittens",
"age": 4
},
{
"id": 2,
"name": "Fluffy",
"age": 3
}
]
}
}
Associated Objects
We want to be able to get the owner of a cat when querying the cat, and also be able to get all the cats of a user when querying the user. This is very easy to achieve in GraphQL. Let's add an additional owner
field to cats
and an additional cats
field to users
:
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import { useCurrentUser } from "src/contexts"
import { db } from "src/providers"
import { cats } from "src/schema"
import * as v from "valibot"
const catResolverFactory = drizzleResolverFactory(db, cats)
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(v.pipe(v.number()))
.derivedFrom("birthday")
.input({
currentYear: v.nullish(v.pipe(v.number(), v.integer()), () =>
new Date().getFullYear()
),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
owner: catResolverFactory.relationField("owner"),
createCats: catResolverFactory.insertArrayMutation().input(
v.pipeAsync(
v.objectAsync({
values: v.arrayAsync(
v.pipeAsync(
v.object({
name: v.string(),
birthday: v.pipe(
v.string(),
v.transform((x) => new Date(x))
),
}),
v.transformAsync(async ({ name, birthday }) => ({
name,
birthday,
ownerId: (await useCurrentUser()).id,
}))
)
),
})
)
),
})
import { mutation, query, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import { eq } from "drizzle-orm"
import { useCurrentUser } from "src/contexts"
import { db } from "src/providers"
import { users } from "src/schema"
import * as v from "valibot"
const userResolverFactory = drizzleResolverFactory(db, users)
export const userResolver = resolver.of(users, {
cats: userResolverFactory.relationField("cats"),
mine: query(users).resolve(() => useCurrentUser()),
usersByName: query(users.$list())
.input({ name: v.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: v.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: v.object({
name: v.string(),
phone: v.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
In the above code, we used the resolver factory to create the owner
field for cats
; similarly, we also created the cats
field for users
. Behind the scenes, the relationship fields created by the resolver factory will use DataLoader
to query from the database to avoid the N+1 problem.
Let's try to query the owner of a cat in the playground:
{
cats {
id
name
age
owner {
id
name
phone
}
}
}
{
"data": {
"cats": [
{
"id": 1,
"name": "Mittens",
"age": 4,
"owner": {
"id": 1,
"name": "Bob",
"phone": "001"
}
},
{
"id": 2,
"name": "Fluffy",
"age": 3,
"owner": {
"id": 1,
"name": "Bob",
"phone": "001"
}
}
]
}
}
Let's try to query the cats of the current user:
{
mine {
name
cats {
id
name
age
}
}
}
{
"authorization": "001"
}
{
"data": {
"mine": {
"name": "Bob",
"cats": [
{
"id": 1,
"name": "Mittens",
"age": 4
},
{
"id": 2,
"name": "Fluffy",
"age": 3
}
]
}
}
}
Conclusion
In this article, we created a simple GraphQL server-side application. We used the following tools:
Valibot
orZod
: Used to define and validate inputs;Drizzle
: Used to operate the database, and directly use theDrizzle
table as theGraphQL
output type;- Context: Used to share data between different parts of the program, which is very useful for scenarios such as implementing login and tracking logs;
- Resolver factory: Used to quickly create resolvers and operations;
GraphQL Yoga
: Used to create a GraphQL HTTP service and provides a GraphiQL playground;
Our application has implemented the functions of adding and querying users
and cats
, but due to space limitations, the update and delete functions have not been implemented. They can be quickly added through the resolver factory.
Next Steps
- Check out the core concepts of GQLoom: Silk, Resolver, Weave;
- Learn about common functions: Context, DataLoader, Middleware
- Add a GraphQL client to the front-end project: gql.tada, Urql, Apollo Client, TanStack Query, Graffle