Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
IT Security Toolsconpot v1.0.0(21.09.2026 um 07:32 Uhr)
IT Security ToolsZircolite v4.0.0(21.09.2026 um 08:27 Uhr)
IT Security NachrichtenWaterPlum Hackers Steal $10.7M in Crypto From IT Workers(21.09.2026 um 08:52 Uhr)
Sicherheitslücken (CVE)Die größte Schwachstelle sitzt am Schreibtisch - kommunal.at(21.09.2026 um 07:36 Uhr)
IT Security NachrichtenIT Security News Hourly Summary 2026-09-21 08h : 6 posts(21.09.2026 um 08:00 Uhr)
IT Security Toolsconpot v1.0.0(21.09.2026 um 07:32 Uhr)
IT Security ToolsZircolite v4.0.0(21.09.2026 um 08:27 Uhr)
IT Security NachrichtenWaterPlum Hackers Steal $10.7M in Crypto From IT Workers(21.09.2026 um 08:52 Uhr)
Sicherheitslücken (CVE)Die größte Schwachstelle sitzt am Schreibtisch - kommunal.at(21.09.2026 um 07:36 Uhr)
IT Security NachrichtenIT Security News Hourly Summary 2026-09-21 08h : 6 posts(21.09.2026 um 08:00 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Getting Started with GQLoom - Building GraphQL Server Applications with Zod + Drizzle

Reagiere als Erste:r — dein Feedback zählt!

Introduction

In the Node.js ecosystem, building GraphQL APIs typically requires writing a lot of boilerplate code manually: defining schemas, writing resolvers, handling type validation, database operations, and more. Today, I'd like to introduce you to a brand new solution — GQLoom — that allows you to build type-safe GraphQL APIs using your favorite TypeScript type libraries (like Valibot or Zod), significantly boosting development efficiency.

What is GQLoom?

GQLoom is a Code First GraphQL Schema framework that weaves runtime types from the TypeScript/JavaScript ecosystem into GraphQL schemas. Simply put, you can use types defined with Valibot or Zod to directly generate GraphQL APIs without duplicating schema definitions.

Hands-on Project: Cattery Management System

To help you better understand the power of GQLoom, we'll build a complete cattery management system together. This project will cover:

  • 🐱 Cat Information Management: Enter, query, and update basic cat information (name, birthday, etc.)
  • 👤 User (Cat Owner) Management: User registration, login authentication, and viewing their own cats
  • 🔗 Relationship Queries: Query cat owner information and view all cats belonging to a user
  • 🛡️ Access Control: Only logged-in users can add cats, ensuring data security

Technology Stack

We'll use the following modern technology stack:

  • 🚀 TypeScript - Type-safe JavaScript superset
  • 🟢 Node.js - JavaScript runtime environment
  • 📊 GraphQL - Powerful API query language
  • 🧘 GraphQL Yoga - Modern GraphQL server
  • 🗄️ Drizzle ORM - Lightweight, type-safe ORM
  • Zod - Powerful TypeScript-first validation library
  • 🧵 GQLoom - Framework for weaving runtime types into GraphQL schemas

Prerequisites

Before we begin, make sure your development environment meets the following requirements:

  • Node.js 20+ - Recommended to use the latest LTS version
  • npm/yarn/pnpm - Package manager (this article uses npm)
  • Code Editor - VS Code or other TypeScript-supporting editor

Creating the Application

Project Structure

Our application will have the following structure:

cattery/
├── src/
│   ├── contexts/
│   │   └── index.ts
│   ├── providers/
│   │   └── index.ts
│   ├── resolvers/
│   │   ├── cat.ts
│   │   ├── index.ts
│   │   └── user.ts
│   ├── schema/
│   │   └── index.ts
│   └── index.ts
├── drizzle.config.ts
├── package.json
└── tsconfig.json

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 schemas, mainly database table structures
  • index.ts: Used to run the GraphQL application as an HTTP service

Tip

GQLoom has no requirements for the project's file structure. This is just a reference, and in practice, you can organize 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

Then, we'll install some necessary dependencies to run a TypeScript application in Node.js:

npm i -D typescript @types/node tsx
npx tsc --init

Next, we'll install GQLoom and Zod-related dependencies:

npm i graphql graphql-yoga @gqloom/core zod @gqloom/zod

Hello World

Let's write our first resolver:

// src/resolvers/index.ts
import { query, resolver } from "@gqloom/core"
import * as z from "zod"

const helloResolver = resolver({
  hello: query(z.string())
    .input({
      name: z
        .string()
        .nullish()
        .transform((x) => x ?? "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:

// src/index.ts
import { createServer } from "node:http"
import { weave } from "@gqloom/core"
import { ZodWeaver } from "@gqloom/zod"
import { createYoga } from "graphql-yoga"
import { resolvers } from "src/resolvers"

const schema = weave(ZodWeaver, ...resolvers)

const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
  console.info("Server is running on http://localhost:4000/graphql")
})

Great! We've created a simple GraphQL application.

Next, let's try running this application. Add the dev script to package.json:

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

Now let's run it:

npm run dev

Open http://localhost:4000/graphql in your browser to see the GraphQL playground.

Let's try sending 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've created the simplest GraphQL application.

Next, we'll use Drizzle ORM to interact with the database and add complete functionality.

Initialize Database and Tables

First, let's install Drizzle ORM. We'll use it to operate the SQLite database.

npm i @gqloom/drizzle drizzle-orm @libsql/client dotenv
npm i -D drizzle-kit

Define Database Tables

Next, define the database tables in the src/schema/index.ts file. We'll define two tables, users and cats, and establish the relationship between them:

// src/schema/index.ts
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:

// drizzle.config.ts
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:

// src/providers/index.ts
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 database in resolvers. We'll create a user resolver and add the following operations:

  • usersByName: Find users by name
  • userByPhone: Find users by phone number
  • createUser: Create a user

After completing the user resolver, we also need to add it to the resolvers in the src/resolvers/index.ts file:

// src/resolvers/user.ts
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import { db } from "src/providers"
import { users } from "src/schema"
import * as z from "zod"

export const userResolver = resolver.of(users, {
  usersByName: query(users.$list())
    .input({ name: z.string() })
    .resolve(({ name }) => {
      return db.query.users.findMany({
        where: eq(users.name, name),
      })
    }),

  userByPhone: query(users.$nullable())
    .input({ phone: z.string() })
    .resolve(({ phone }) => {
      return db.query.users.findFirst({
        where: eq(users.phone, phone),
      })
    }),

  createUser: mutation(users)
    .input({
      data: z.object({
        name: z.string(),
        phone: z.string(),
      }),
    })
    .resolve(async ({ data }) => {
      const [user] = await db.insert(users).values(data).returning()
      return user
    }),
})
// src/resolvers/index.ts
import { query, resolver } from "@gqloom/core"
import { userResolver } from "src/resolvers/user"
import * as z from "zod"

const helloResolver = resolver({
  hello: query(z.string())
    .input({
      name: z
        .string()
        .nullish()
        .transform((x) => x ?? "World"),
    })
    .resolve(({ name }) => `Hello ${name}!`),
})

export const resolvers = [helloResolver, userResolver]

Great! Now let's try it in the playground:

GraphQL Mutation:

mutation {
  createUser(data: {name: "Bob", phone: "001"}) {
    id
    name
    phone
  }
}

Response:

{
  "data": {
    "createUser": {
      "id": 1,
      "name": "Bob",
      "phone": "001"
    }
  }
}

Let's continue to try retrieving the user we just created:

GraphQL Query:

{
  usersByName(name: "Bob") {
    id
    name
    phone
  }
}

Response:

{
  "data": {
    "usersByName": [
      {
        "id": 1,
        "name": "Bob",
        "phone": "001"
      }
    ]
  }
}

Current User Context

First, let's add the asyncContextProvider middleware to enable asynchronous context:

// src/index.ts
import { createServer } from "node:http"
import { weave } from "@gqloom/core"
import { asyncContextProvider } from "@gqloom/core/context"
import { ZodWeaver } from "@gqloom/zod"
import { createYoga } from "graphql-yoga"
import { resolvers } from "src/resolvers"

const schema = weave(asyncContextProvider, ZodWeaver, ...resolvers)

const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
  console.info("Server is running on http://localhost:4000/graphql")
})

Next, let's try adding a simple login function and add a query operation to the user resolver:

  • mine: Return current user information

To implement this query, we first need to have a login function. Let's write a simple one:

// src/contexts/index.ts
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.

Note
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:

// src/resolvers/user.ts
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 z from "zod"

export const userResolver = resolver.of(users, {
  mine: query(users).resolve(() => useCurrentUser()),

  usersByName: query(users.$list())
    .input({ name: z.string() })
    .resolve(({ name }) => {
      return db.query.users.findMany({
        where: eq(users.name, name),
      })
    }),

  userByPhone: query(users.$nullable())
    .input({ phone: z.string() })
    .resolve(({ phone }) => {
      return db.query.users.findFirst({
        where: eq(users.phone, phone),
      })
    }),

  createUser: mutation(users)
    .input({
      data: z.object({
        name: z.string(),
        phone: z.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:

GraphQL Query:

{
  mine {
    id
    name
    phone
  }
}

Response:

{
  "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:

Headers:

{
  "authorization": "001"
}

GraphQL Query:

{
  mine {
    id
    name
    phone
  }
}

Response:

{
  "data": {
    "mine": {
      "id": 1,
      "name": "Bob",
      "phone": "001"
    }
  }
}

Resolver Factory

Next, we'll add business logic related to cats.

We use the resolver factory to quickly create interfaces:

// src/resolvers/cat.ts
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import { db } from "src/providers"
import { cats } from "src/schema"
import * as z from "zod"

const catResolverFactory = drizzleResolverFactory(db, cats)

export const catResolver = resolver.of(cats, {
  cats: catResolverFactory.selectArrayQuery(),

  age: field(z.number())
    .derivedFrom("birthday")
    .input({
      currentYear: z.number().int().nullish().transform((x) => x ?? new Date().getFullYear()),
    })
    .resolve((cat, { currentYear }) => {
      return currentYear - cat.birthday.getFullYear()
    }),
})
// src/resolvers/index.ts
import { query, resolver } from "@gqloom/core"
import { catResolver } from "src/resolvers/cat"
import { userResolver } from "src/resolvers/user"
import * as z from "zod"

const helloResolver = resolver({
  hello: query(z.string())
    .input({
      name: z
        .string()
        .nullish()
        .transform((x) => x ?? "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 adding a createCat mutation. We want only logged-in users to access this interface, and the created cats will belong to the current user:

// src/resolvers/cat.ts (partial code)
createCats: catResolverFactory.insertArrayMutation().input(
  z.object({
    values: z.array(
      z.object({
        name: z.string(),
        birthday: z.string().transform((x) => new Date(x)),
      }).transform(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 adding a few cats in the playground:

GraphQL Mutation:

mutation {
  createCats(data: {values: [{name: "Whiskers", birthday: "2020-01-01"}, {name: "Whiskers", birthday: "2020-01-01"}]}) {
    id
    name
    age
  }
}

Headers:

{
  "authorization": "001"
}

Response:

{
  "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:

GraphQL Query:

{
  cats {
    id
    name   
    age
  }
}

Response:

{
  "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:

// src/resolvers/cat.ts
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 z from "zod"

const catResolverFactory = drizzleResolverFactory(db, cats)

export const catResolver = resolver.of(cats, {
  cats: catResolverFactory.selectArrayQuery(),

  age: field(z.number())
    .derivedFrom("birthday")
    .input({
      currentYear: z.number().int().nullish().transform((x) => x ?? new Date().getFullYear()),
    })
    .resolve((cat, { currentYear }) => {
      return currentYear - cat.birthday.getFullYear()
    }),

  owner: catResolverFactory.relationField("owner"),

  createCats: catResolverFactory.insertArrayMutation().input(
    z.object({
      values: z.array(
        z.object({
          name: z.string(),
          birthday: z.string().transform((x) => new Date(x)),
        }).transform(async ({ name, birthday }) => ({
          name,
          birthday,
          ownerId: (await useCurrentUser()).id,
        }))
      ),
    })
  ),
})
// src/resolvers/user.ts
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 z from "zod"

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: z.string() })
    .resolve(({ name }) => {
      return db.query.users.findMany({
        where: eq(users.name, name),
      })
    }),

  userByPhone: query(users.$nullable())
    .input({ phone: z.string() })
    .resolve(({ phone }) => {
      return db.query.users.findFirst({
        where: eq(users.phone, phone),
      })
    }),

  createUser: mutation(users)
    .input({
      data: z.object({
        name: z.string(),
        phone: z.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 querying the owner of a cat in the playground:

GraphQL Query:

{
  cats {
    id
    name
    age
    owner {
      id
      name
      phone
    }
  }
}

Response:

{
  "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 querying the cats of the current user:

GraphQL Query:

{
  mine {
    name
    cats {
      id
      name
      age
    }
  }
}

Headers:

{
  "authorization": "001"
}

Response:

{
  "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:

  • Zod: Used to define and validate inputs
  • Drizzle: Used to operate the database, and directly use the Drizzle table as the GraphQL 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

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Getting Started with GQLoom - Building GraphQL Server Applications with Zod + Drizzle

Thematisch verwandte Begriffe: Getting, Started, with, GQLoom · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94030 | A security vulnerability has been detected in SerenityOS up to 3d83e4509…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick