Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
41 changes: 41 additions & 0 deletions graphql-demo-nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
91 changes: 91 additions & 0 deletions graphql-demo-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# GraphQL Demo (Next.js)

A stateful GraphQL endpoint generator.

This project is a Next.js port of the original Cloudflare Worker “graphql-demo,” offering both a Redis-backed server mode and a client-only localStorage mode.

---

## Features

- **Admin endpoint** at `/api/admin` to create new slug-based GraphQL endpoints.
- **Token generator** at `/api/token` to produce long-lived JWTs.
- **Per-slug endpoint** at `/api/[slug]` serving GraphQL Playground (HTML) and JSON queries.
- **Home page** lists all existing endpoints (Redis mode) and explains how to use the demo.

---

## Getting Started

1. **Clone** this repo and install dependencies:

```bash
git clone <your-repo-url>
cd graphql-demo-nextjs
npm install
```

2. **Environment**

Copy `.env.local.example` → `.env.local` and fill in:

```dotenv
UPSTASH_REDIS_REST_URL=https://<your-upstash-id>.upstash.io
UPSTASH_REDIS_REST_TOKEN=<your-upstash-rest-token>
TOKEN_SECRET=<your-jwt-secret>
ALLOWED_ORIGINS=http://localhost:3000
```

3. **Run**

```bash
npm run dev
```

- Open `http://localhost:3000` for home page
- `/api/token`, `/api/admin`, `/api/<slug>` are live

4. **Deploy**

Push to GitHub, import into Vercel, set the same env vars, and deploy.

---

## Usage Overview

1. **Generate Token**

```bash
curl http://localhost:3000/api/token
# → { "token": "<your-jwt-here>" }
```

2. **Create Endpoint**

- Open GraphQL Playground at `http://localhost:3000/api/admin`
- Click **HTTP HEADERS** and add:

```json
{ "Authorization": "bearer <your-jwt-here>" }
```

- Run:

```graphql
mutation {
createEndpoint(slug: "my-first-slug")
}
```

3. **Query Your Slug**

- Playground: `http://localhost:3000/api/my-first-slug`
- Or via `curl`:

```bash
curl -X POST http://localhost:3000/api/my-first-slug \
-H "Content-Type: application/json" \
-d '{"query":"query { todos { id title } }"}'
```

---
25 changes: 25 additions & 0 deletions graphql-demo-nextjs/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const compat = new FlatCompat({
baseDirectory: __dirname,
})

const eslintConfig = [
...compat.config({
extends: ['next/core-web-vitals', 'next/typescript'],
rules: {
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'react-hooks/rules-of-hooks': 'off',
'react/no-unescaped-entities': 'off',
'@next/next/no-page-custom-font': 'off',
},
}),
]

export default eslintConfig
8 changes: 8 additions & 0 deletions graphql-demo-nextjs/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
/* config options here */
reactStrictMode: true,
}

export default nextConfig
39 changes: 39 additions & 0 deletions graphql-demo-nextjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "graphql-demo-nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"token": "node token.js"
},
"dependencies": {
"@envelop/core": "^2.0.0",
"@graphql-tools/schema": "^9.0.0",
"@tsndr/cloudflare-worker-jwt": "^1.1.5",
"@upstash/redis": "^1.19.0",
"graphql": "^16.7.1",
"graphql-playground-html": "^1.6.30",
"graphql-tag": "^2.12.6",
"jsonwebtoken": "9.0.2",
"next": "15.3.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"slugify": "^1.6.5",
"uuid": "^8.3.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"tailwindcss": "^4",
"typescript": "^5"
}
}
5 changes: 5 additions & 0 deletions graphql-demo-nextjs/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const config = {
plugins: ['@tailwindcss/postcss'],
}

export default config
Binary file added graphql-demo-nextjs/public/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions graphql-demo-nextjs/public/file.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions graphql-demo-nextjs/public/globe.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions graphql-demo-nextjs/public/next.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions graphql-demo-nextjs/public/vercel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions graphql-demo-nextjs/public/window.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions graphql-demo-nextjs/src/admin/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { makeExecutableSchema } from '@graphql-tools/schema'
import gql from 'graphql-tag'
import { createState } from '../utils'
import type { AdminContext } from '../types'

const typeDefs = gql`
type Query {
_empty: String
}

type Mutation {
createEndpoint(slug: String!): String!
}
`

const resolvers = {
Mutation: {
createEndpoint: async (
_: any,
{ slug }: { slug: string },
context: AdminContext,
) => {
if (await context.env.STATE.get(slug)) {
throw new Error(`Endpoint with slug ${slug} already taken`)
}
await context.env.STATE.set(slug, JSON.stringify(createState()))
return slug
},
},
}

export const adminSchema = makeExecutableSchema({ typeDefs, resolvers })
111 changes: 111 additions & 0 deletions graphql-demo-nextjs/src/mock/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { makeExecutableSchema } from '@graphql-tools/schema'
import gql from 'graphql-tag'
import type { EndpointContext } from '../types'

const typeDefs = gql`
type User {
id: ID!
name: String!
todos: [Todo!]!
}
type Todo {
id: ID!
title: String!
createdAt: String!
user: User
}
type Query {
user(id: ID!): User!
users: [User!]!
todo(id: ID!): Todo!
todos: [Todo!]!
}
type Mutation {
addUser(name: String!): User!
updateUser(id: ID!, name: String!): User!
deleteUser(id: ID!): User!
addTodo(userId: ID!, title: String!): Todo!
updateTodo(id: ID!, title: String!): Todo!
deleteTodo(id: ID!): Todo!
}
`

const resolvers = {
User: {
todos: (root: any, _args: any, ctx: EndpointContext) =>
ctx.state.todos.filter((t) => t.user === root.id),
},
Todo: {
user: (root: any, _args: any, ctx: EndpointContext) =>
ctx.state.users.find((u) => u.id === root.user)!,
},
Query: {
users: (_: any, __: any, ctx: EndpointContext) => ctx.state.users,
user: (_: any, { id }: { id: string }, ctx: EndpointContext) =>
ctx.state.users.find((u) => u.id === id)!,
todos: (_: any, __: any, ctx: EndpointContext) => ctx.state.todos,
todo: (_: any, { id }: { id: string }, ctx: EndpointContext) =>
ctx.state.todos.find((t) => t.id === id)!,
},
Mutation: {
addUser: (_: any, { name }: { name: string }, ctx: EndpointContext) => {
const newUser = { id: String(ctx.state.users.length + 1), name }
ctx.setState({ ...ctx.state, users: [...ctx.state.users, newUser] })
return newUser
},
updateUser: (
_: any,
{ id, name }: { id: string; name: string },
ctx: EndpointContext,
) => {
const users = ctx.state.users.map((u) =>
u.id === id ? { ...u, name } : u,
)
ctx.setState({ ...ctx.state, users })
return users.find((u) => u.id === id)!
},
deleteUser: (_: any, { id }: { id: string }, ctx: EndpointContext) => {
const user = ctx.state.users.find((u) => u.id === id)!
ctx.setState({
...ctx.state,
users: ctx.state.users.filter((u) => u.id !== id),
})
return user
},
addTodo: (
_: any,
{ userId, title }: { userId: string; title: string },
ctx: EndpointContext,
) => {
const newTodo = {
id: String(ctx.state.todos.length + 1),
title,
user: userId,
createdAt: new Date().toISOString(),
}
ctx.setState({ ...ctx.state, todos: [...ctx.state.todos, newTodo] })
return newTodo
},
updateTodo: (
_: any,
{ id, title }: { id: string; title: string },
ctx: EndpointContext,
) => {
const todos = ctx.state.todos.map((t) =>
t.id === id ? { ...t, title } : t,
)
ctx.setState({ ...ctx.state, todos })
return todos.find((t) => t.id === id)!
},
deleteTodo: (_: any, { id }: { id: string }, ctx: EndpointContext) => {
const todo = ctx.state.todos.find((t) => t.id === id)!
ctx.setState({
...ctx.state,
todos: ctx.state.todos.filter((t) => t.id !== id),
})
return todo
},
},
}

export const userSchema = makeExecutableSchema({ typeDefs, resolvers })
6 changes: 6 additions & 0 deletions graphql-demo-nextjs/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import '@/styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
13 changes: 13 additions & 0 deletions graphql-demo-nextjs/src/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
return (
<Html lang='en'>
<Head />
<body className='antialiased'>
<Main />
<NextScript />
</body>
</Html>
)
}
Loading