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
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!