Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions examples/graphql-pothos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
Build a GraphQL API using Pothos for schema definition and GraphQL Yoga for request handling. This example demonstrates type-safe schema construction with Pothos object refs and field resolvers, served through Nitro's file-based routing.

## GraphQL Route

```ts [server/routes/graphql.ts]
import { createYoga } from "graphql-yoga";
import { defineEventHandler, defineLazyEventHandler, type H3Event } from "nitro/h3";
import { schema } from "../graphql/schema";

export default defineLazyEventHandler(() => {
const yoga = createYoga<{ event: H3Event }>({
schema,
fetchAPI: { Response },
});

return defineEventHandler((event) => {
return yoga.handleRequest(event.req, { event });
});
});
```

The route uses `defineLazyEventHandler` to create the Yoga instance once and reuse it across requests. The GraphQL endpoint is available at `/graphql`.

## Schema Builder

```ts [server/graphql/builder.ts]
import SchemaBuilder from "@pothos/core";
import type { H3Event } from "nitro/h3";

export const builder = new SchemaBuilder<{
Context: { event: H3Event };
}>({});
builder.queryType({});
```

Create a typed schema builder with the H3 event in context. Schema files are auto-loaded via `import.meta.glob`:

```ts [server/graphql/schema.ts]
import { builder } from "./builder";

import.meta.glob("./schema/**/*.ts", { eager: true });

export const schema = builder.toSchema();
```

## Defining Types

Each type is defined in its own file using Pothos object refs:

```ts [server/graphql/schema/post.ts]
import { Comments, type IPost, Posts, Users } from "../../utils/data";
import { builder } from "../builder";
import { Comment } from "./comment";
import { User } from "./user";

export const Post = builder.objectRef<IPost>("Post");

Post.implement({
fields: (t) => ({
id: t.exposeID("id"),
title: t.exposeString("title"),
content: t.exposeString("content"),
author: t.field({
type: User,
nullable: true,
resolve: (post) => [...Users.values()].find((user) => user.id === post.authorId),
}),
comments: t.field({
type: [Comment],
resolve: (post) => [...Comments.values()].filter((comment) => comment.postId === post.id),
}),
}),
});

builder.queryFields((t) => ({
post: t.field({
type: Post,
nullable: true,
args: {
id: t.arg.id({ required: true }),
},
resolve: (_root, args) => Posts.get(String(args.id)),
}),
}));
```

Use `builder.objectRef` to bind TypeScript types to GraphQL types, then implement fields with resolvers. Use `builder.queryFields` to define the entry points for fetching data.
5 changes: 5 additions & 0 deletions examples/graphql-pothos/nitro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineConfig } from "nitro";

export default defineConfig({
serverDir: "./server",
});
21 changes: 21 additions & 0 deletions examples/graphql-pothos/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@pothos-examples/nitro",
"version": "3.1.22",
"private": true,
"scripts": {
"build": "vite build",
"dev": "vite dev",
"start": "node .output/server/index.mjs",
"type": "tsc --noEmit"
},
"dependencies": {
"@faker-js/faker": "^10.1.0",
"@pothos/core": "^4.12.0",
"graphql": "^16.10.0",
"graphql-yoga": "5.17.1"
},
"devDependencies": {
"nitro": "^3.0.1-alpha.2",
"vite": "^8.0.0-beta.16"
}
}
16 changes: 16 additions & 0 deletions examples/graphql-pothos/server/graphql/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import SchemaBuilder from "@pothos/core";
import type { H3Event } from "nitro/h3";

export const builder = new SchemaBuilder<{
Context: { event: H3Event };
}>({});
builder.queryType({});
// builder.mutationType({})
// builder.subscriptionType({})

// Do not include in production
if (import.meta.dev) {
// Tell vite to reload the builder when schema changes
// https://github.com/hayes/pothos/issues/49#issuecomment-836056530
import("./schema");
}
6 changes: 6 additions & 0 deletions examples/graphql-pothos/server/graphql/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { builder } from "./builder";

// Run all sideffects to add the fields
Comment thread
Teages marked this conversation as resolved.
Outdated
import.meta.glob("./schema/**/*.ts", { eager: true });

export const schema = builder.toSchema();
37 changes: 37 additions & 0 deletions examples/graphql-pothos/server/graphql/schema/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type IComment, Posts, Users } from "../../utils/data";
import { builder } from "../builder";
import { Post } from "./post";
import { User } from "./user";

export const Comment = builder.objectRef<IComment>("Comment");

const DEFAULT_PAGE_SIZE = 10;

Comment.implement({
fields: (t) => ({
id: t.exposeID("id"),
comment: t.exposeString("comment"),
author: t.field({
type: User,
nullable: true,
resolve: (comment) => [...Users.values()].find((user) => user.id === comment.authorId),
}),
post: t.field({
type: Post,
resolve: (comment) => [...Posts.values()].find((post) => post.id === comment.postId),
}),
}),
});

builder.queryFields((t) => ({
posts: t.field({
type: [Post],
nullable: true,
args: {
take: t.arg.int(),
skip: t.arg.int(),
},
resolve: (_root, { skip, take }) =>
[...Posts.values()].slice(skip ?? 0, (skip ?? 0) + (take ?? DEFAULT_PAGE_SIZE)),
Comment thread
Teages marked this conversation as resolved.
Outdated
}),
}));
34 changes: 34 additions & 0 deletions examples/graphql-pothos/server/graphql/schema/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Comments, type IPost, Posts, Users } from "../../utils/data";
import { builder } from "../builder";
import { Comment } from "./comment";
import { User } from "./user";

export const Post = builder.objectRef<IPost>("Post");

Post.implement({
fields: (t) => ({
id: t.exposeID("id"),
title: t.exposeString("title"),
content: t.exposeString("content"),
author: t.field({
type: User,
nullable: true,
resolve: (post) => [...Users.values()].find((user) => user.id === post.authorId),
}),
comments: t.field({
type: [Comment],
resolve: (post) => [...Comments.values()].filter((comment) => comment.postId === post.id),
}),
}),
});

builder.queryFields((t) => ({
post: t.field({
type: Post,
nullable: true,
args: {
id: t.arg.id({ required: true }),
},
resolve: (_root, args) => Posts.get(String(args.id)),
}),
}));
36 changes: 36 additions & 0 deletions examples/graphql-pothos/server/graphql/schema/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Comments, type IUser, Posts, Users } from "../../utils/data";
import { builder } from "../builder";
import { Comment } from "./comment";
import { Post } from "./post";

export const User = builder.objectRef<IUser>("User");

User.implement({
fields: (t) => ({
id: t.exposeID("id"),
firstName: t.exposeString("firstName"),
lastName: t.exposeString("lastName"),
fullName: t.string({
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
posts: t.field({
type: [Post],
resolve: (user) => [...Posts.values()].filter((post) => post.authorId === user.id),
}),
comments: t.field({
type: [Comment],
resolve: (user) => [...Comments.values()].filter((comment) => comment.authorId === user.id),
}),
}),
});

builder.queryFields((t) => ({
user: t.field({
type: User,
nullable: true,
args: {
id: t.arg.id({ required: true }),
},
resolve: (_root, args) => Users.get(String(args.id)),
}),
}));
14 changes: 14 additions & 0 deletions examples/graphql-pothos/server/routes/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createYoga } from "graphql-yoga";
import { defineEventHandler, defineLazyEventHandler, type H3Event } from "nitro/h3";
import { schema } from "../graphql/schema";

export default defineLazyEventHandler(() => {
const yoga = createYoga<{ event: H3Event }>({
schema,
fetchAPI: { Response },
});

return defineEventHandler((event) => {
return yoga.handleRequest(event.req, { event });
});
});
50 changes: 50 additions & 0 deletions examples/graphql-pothos/server/utils/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { faker } from "@faker-js/faker";

export interface IUser {
id: string;
firstName: string;
lastName: string;
}

export interface IPost {
id: string;
authorId: string;
title: string;
content: string;
}

export interface IComment {
id: string;
postId: string;
authorId: string;
comment: string;
}

export const Users = new Map<string, IUser>();
export const Posts = new Map<string, IPost>();
export const Comments = new Map<string, IComment>();

faker.seed(123);

// Create 100 users, posts and comments
for (let i = 1; i <= 100; i += 1) {
Users.set(String(i), {
id: String(i),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
});

Posts.set(String(i), {
id: String(i),
authorId: String(faker.number.int({ min: 1, max: 100 })),
title: faker.lorem.text(),
content: faker.lorem.paragraphs(2),
});

Comments.set(String(i), {
id: String(i),
authorId: String(faker.number.int({ min: 1, max: 100 })),
postId: String(faker.number.int({ min: 1, max: 100 })),
comment: faker.lorem.text(),
});
}
3 changes: 3 additions & 0 deletions examples/graphql-pothos/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "nitro/tsconfig"
}
6 changes: 6 additions & 0 deletions examples/graphql-pothos/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { nitro } from "nitro/vite";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [nitro()],
});
Loading