Skip to main content

Tutorial

Let's create a blog with graphql-magic!

Setup

Code base

First create a next.js website:

npx create-next-app@latest magic-blog --ts --app --tailwind --eslint --src-dir
cd magic-blog

Replace src/app/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

main {
@apply w-96 mx-auto
}

nav {
@apply flex items-center
}

h1, h2, h3, h4 {
@apply font-bold
}

h1 {
@apply text-4xl mb-4 flex-grow
}

h2 {
@apply text-3xl mb-3
}

h3 {
@apply text-2xl mb-2
}

h4 {
@apply text-xl mb-1
}

a {
@apply text-blue-500
}

article, form {
@apply mb-4 p-3 rounded-lg shadow-md border border-gray-100
}

input, textarea {
@apply border border-gray-300 w-full rounded-md p-1
}

label span {
@apply font-bold
}

Replace src/app/page.tsx:

export default async function Home() {
return <main>
<nav>
<h1>Magic Blog</h1>
</nav>
</main>
}

Start the website:

npm run dev

Install graphql-magic

Add this setting to next.config.mjs:

const nextConfig = {
experimental: {
serverComponentsExternalPackages: ['knex'],
}
};

Install @smartive/graphql-magic and needed dependencies:

npm install @smartive/graphql-magic @graphql-codegen/typescript-compatibility

Run the gqm cli:

npx gqm generate

Database setup

Let's boot a local database instance. Create the following docker-compose.yml:

version: '3.4'
services:
postgres:
image: postgres:13-alpine
shm_size: 1gb
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- '5432:5432'

Then start it with docker-compose up.

Generate the first migration:

npx gqm generate-migration

Enter a migration name, e.g. "setup".

Run the migration

npx env-cmd knex migrate:latest

Auth setup

Set up a way for users to authenticate with your app. For example, follow this tutorial to set up auth0.

Assuming you used auth0, here's a bare-bones version of what src/app/page.tsx could look like:

import { getSession } from '@auth0/nextjs-auth0';

export default async function Page() {
const session = await getSession();

return <main>
<nav>
<h1>Welcome to my Blog</h1>
{session ? <a href="/api/auth/logout">Logout</a> : <a href="/api/auth/login">Login</a>}
</nav>
</main>
}

It should now be possible for you to log in and out again.

Account setup

Now, we need to ensure that the user is stored in the database.

First extend the user model in src/config/models.ts with the following fields:

    fields: [
{
name: 'authId',
type: 'String',
nonNull: true,
},
{
name: 'username',
type: 'String',
nonNull: true
}
]

The models have changed, generate the new types:

npx gqm generate

Generate the new migration:

npx gqm generate-migration

Edit the generated migration, then run it

npx env-cmd knex migrate:latest

Now let's implement the // TODO: get user part in the src/graphql/execute.ts file

  const session = await getSession();
if (session) {
let dbUser = await db('User').where({ authId: session.user.sub }).first();
if (!user) {
await db('User').insert({
id: randomUUID(),
authId: session.user.sub,
username: session.user.nickname
})
dbUser = await db('User').where({ authId: session.user.sub }).first();
}
user = {
...dbUser!,
role: 'ADMIN'
}
}

Extend src/graphql/client/queries/get-me.ts to also fetch the user's username:

import { gql } from '@smartive/graphql-magic';

export const GET_ME = gql`
query GetMe {
me {
id
username
}
}
`;

Generate the new types:

npx gqm generate

Now, let's modify src/app/page.tsx so that it fetches the user from the database:

import { GetMeQuery } from "@/generated/client";
import { GET_ME } from "@/graphql/client/queries/get-me";
import { executeGraphql } from "@/graphql/execute";

export default async function Home() {
const { data: { me } } = await executeGraphql<GetMeQuery>({ query: GET_ME });

return (
<main>
<nav>
<h1>Blog</h1>
{me ? <span>Hello, {me.username}! <a href="/api/auth/logout"> Logout</a></span> : <a href="/api/auth/login">Login</a>}
</nav>
</main>
);
}

Content!

Let's make a blog out of this app by adding new models in src/config/models.ts:

  {
kind: 'entity',
name: 'Post',
listQueriable: true,
creatable: true,
updatable: true,
deletable: true,
fields: [
{
name: 'title',
type: 'String',
nonNull: true,
creatable: true,
updatable: true,
},
{
name: 'content',
type: 'String',
nonNull: true,
creatable: true,
updatable: true,
}
]
},
{
kind: 'entity',
name: 'Comment',
creatable: true,
updatable: true,
deletable: true,
fields: [
{
kind: 'relation',
name: 'post',
type: 'Post',
nonNull: true,
creatable: true,
},
{
name: 'content',
type: 'String',
nonNull: true,
creatable: true,
updatable: true,
}
]
}

Generate and run the new migrations and generate the new models:

npx gqm generate-migration
npx env-cmd knex migrate:latest

Create a new query src/graphql/client/queries/get-posts.ts:

import { gql } from '@smartive/graphql-magic';

export const GET_POSTS = gql`
query GetPosts {
posts {
id
title
content
createdBy {
username
}
comments {
id
createdBy {
username
}
content
}
}
}
`;

Generate the new types:

npx gqm generate

Now add all the logic to create and display posts and comments to src/app/page.tsx

import { CreateCommentMutationMutation, CreateCommentMutationMutationVariables, CreatePostMutationMutation, CreatePostMutationMutationVariables, GetMeQuery, GetPostsQuery } from "@/generated/client";
import { CREATE_COMMENT, CREATE_POST } from "@/generated/client/mutations";
import { GET_ME } from "@/graphql/client/queries/get-me";
import { GET_POSTS } from "@/graphql/client/queries/get-posts";
import { executeGraphql } from "@/graphql/execute";
import { revalidatePath } from "next/cache";

export default async function Home() {
const { data: { me } } = await executeGraphql<GetMeQuery>({ query: GET_ME });

return (
<main>
<nav>
<h1>Blog</h1>
{me ? <span>Hello, {me.username}! <a href="/api/auth/logout"> Logout</a></span> : <a href="/api/auth/login">Login</a>}
</nav>
{me && <CreatePost />}
<Posts me={me} />
</main>
);
}

async function Posts({ me }: { me: GetMeQuery['me'] }) {
const { data: { posts } } = await executeGraphql<GetPostsQuery>({ query: GET_POSTS })

return <div>
{posts.map(post => <div key={post.id}>
<article>
<h2>{post.title}</h2>
<div>by {post.createdBy.username}</div>
<p>{post.content}</p>
<h4>Comments</h4>
{post.comments.map(comment => (<div key={comment.id}>
<div>{comment.createdBy.username}</div>
<p>{comment.content}</p> by {comment.createdBy.username}
</div>)
)}
{me && <CreateComment postId={post.id} />}
</article>
</div>)}
</div>
}

async function CreatePost() {
async function createPost(formData: FormData) {
'use server'
await executeGraphql<CreatePostMutationMutation, CreatePostMutationMutationVariables>({
query: CREATE_POST,
variables: {
data: {
title: formData.get('title') as string,
content: formData.get('content') as string
}
}
})
revalidatePath('/')
}

return <form action={createPost}>
<h2>New Post</h2>
<label>
<span>Title</span>
<input name="title" />
</label>
<label>
<span>Content</span>
<textarea rows={5} name="content" />
</label>
<div>
<button type="submit">Create</button>
</div>
</form>
}

function CreateComment({ postId }: { postId: string }) {
async function createComment(formData: FormData) {
'use server'

const res = await executeGraphql<CreateCommentMutationMutation, CreateCommentMutationMutationVariables>({
query: CREATE_COMMENT,
variables: {
data: {
postId,
content: formData.get('content') as string
}
}
})
console.log(res)
revalidatePath('/')
}
return <form action={createComment}>
<div>
<textarea name="content" placeholder="Leave a comment..." />
</div>
<div>
<button type="submit">Send</button>
</div>
</form>
}

Now you should have a working minimal blog example!