Build Fresh backends with GraphQL

Build Fresh backends with GraphQL

Fresh is the next-gen full stack web framework built for speed, reliability, and simplicity. Fresh uses Preact and JSX for rendering on the server and client, so if you'll most likely feel at home if you've used another modern JavaScript framework over the last few years.

Fresh doesn't ship any JavaScript to the browser unless you tell it to. Fresh embraces progressive enhancement on the client, and renders everything on the server by default.

In this guide we'll be using Deno, Fresh, and Grafbase to build a Guestbook. We'll call it... Grafbook!

Make sure to install Deno and create your Fresh app via the command line:

deno run -A -r https://fresh.deno.dev grafbook

Now make sure you install Grafbase CLI and run the following command inside of the grafbook directory created previously:

npx grafbase@latest init

This will generate the file grafbase/schema.graphql. Open this file and replace the contents with a simple @model:

type Message @model {
  author: String!
  message: String!
}

This is all we need to create a backend for our Fresh app! Run the Grafbase CLI to generate a GraphQL backend:

npx grafbase@latest dev

Be sure to create the file .env in the root of your project with the following:

GRAFBASE_API_URL=http://localhost:4000/graphql
GRAFBASE_API_KEY=

Then inside main.ts add the following import:

import 'https://deno.land/std@0.145.0/dotenv/load.ts'

You can ignore the value for GRAFBASE_API_KEY in development. You'll want to make sure to use your Grafbase deployment URL and API key in production.

Next create the file utils/grafbase.ts in the root of your project, and export the following function:

export const grafbaseClient = ({
  query,
  variables
}: {
  query: string | string[]
  // deno-lint-ignore no-explicit-any
  variables: { [key: string]: any }
}) =>
  fetch(Deno.env.get('GRAFBASE_API_URL') as string, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-api-key': Deno.env.get('GRAFBASE_API_KEY') as string
    },
    body: JSON.stringify({
      query,
      variables
    })
  })

This function has two named arguments:

  • query
  • variables

Next update the import_map.json to include this new file:

{
  "imports": {
    "@/utils/grafbase": "./utils/grafbase.ts"
  }
}

We can then import the grafbaseClient and send GraphQL requests to our backend from the server with Fresh handlers. We'll do that next.

Inside routes/index.tsx you can safely remove all of the contents and import the following:

import { Handlers, PageProps } from '$fresh/server.ts'
import { grafbaseClient } from '@/utils/grafbase'

Now we will write our first GraphQL query that will fetch all messages from our backend. Below the imports we added above you can now add the const GetAllMessagesQuery:

import { Handlers, PageProps } from '$fresh/server.ts'
import { grafbaseClient } from '@/utils/grafbase'

const GetAllMessagesQuery = /* GraphQL */ `
  query GetAllMessages($first: Int!) {
    messageCollection(first: $first) {
      edges {
        node {
          id
          author
          message
          createdAt
        }
      }
    }
  }
`

Next we will export the handler with a custom GET action:

export const handler: Handlers = {
  async GET(_, ctx) {
    const response = await grafbaseClient({
      query: GetAllMessagesQuery,
      variables: {
        first: 100
      }
    })

    if (!response.ok) {
      return ctx.render(null)
    }

    const { data } = await response.json()

    return ctx.render(data)
  }
}

Finally all that's left to do is render the messageCollection.edges. We can do this by exporting a new default JSX component:

export default function IndexPage({ data }) {
  return (
    <>
      <h1>Grafbook</h1>

      <ul>
        {data?.messageCollection?.edges?.map(({ node }) => (
          <li key={node.id}>
            <p>
              <strong>
                <a href={`/messages/${node.id}`}>{node.author}</a>
                <br />
                <small>
                  {new Intl.DateTimeFormat('en-GB', {
                    dateStyle: 'medium',
                    timeStyle: 'short'
                  }).format(Date.parse(node.createdAt))}
                </small>
              </strong>
            </p>
            <p>{node.message}</p>
          </li>
        ))}
      </ul>
    </>
  )
}

It's recommended you generate TypeScript types for your GraphQL operations and use those with Fresh. For the purposes of this guide we'll skip the ceremony by creating the type Message inside of our route:

type Message = {
  id: string
  author: string
  message: string
  createdAt: string
}

You can then pass the type to PageProps:

export default function IndexPage({
  data
}: PageProps<{ messageCollection: { edges: { node: Message }[] } }>) {
  // ...
}

You can now start the Fresh and Grafbase dev server but there will be no posts shown! We'll fix that next.

So users can post messages to our Grafbook we will want to add a form that on submit saves the data in our GraphQL backend.

Inside routes/index.tsx you will want to add the following <form>:

export default function IndexPage({
  data
}: PageProps<{ messageCollection: { edges: { node: Message }[] } }>) {
  return (
    <>
      <h1>Grafbook</h1>
      <form method="POST">
        <fieldset>
          <legend>New message</legend>
          <input id="author" name="author" placeholder="Name" />
          <br />
          <textarea
            id="message"
            name="message"
            placeholder="Write a message..."
            rows={5}
          ></textarea>
          <br />
          <button type="submit">Submit</button>
        </fieldset>
      </form>
      <ul>
        {data?.messageCollection?.edges?.map(({ node }) => (
          <li key={node.id}>
            <p>
              <strong>
                <a href={`/messages/${node.id}`}>{node.author}</a>
                <br />
                <small>
                  {new Intl.DateTimeFormat('en-GB', {
                    dateStyle: 'medium',
                    timeStyle: 'short'
                  }).format(Date.parse(node.createdAt))}
                </small>
              </strong>
            </p>
            <p>{node.message}</p>
          </li>
        ))}
      </ul>
    </>
  )
}

So this form actually does something when submitted we can define a custom POST action inside of our handler:

const AddNewMessageMutation = /* GraphQL */ `
  mutation AddNewMessage($author: String!, $message: String!) {
    messageCreate(input: { author: $author, message: $message }) {
      message {
        id
      }
    }
  }
`

export const handler: Handlers = {
  async GET(_, ctx) {
    // ...
  },
  async POST(req, ctx) {
    const formData = await req.formData()
    const json = Object.fromEntries(formData)

    await grafbaseClient({
      query: AddNewMessageMutation,
      variables: {
        author: json.author,
        message: json.message
      }
    })

    const response = await grafbaseClient({
      query: GetAllMessagesQuery,
      variables: {
        first: 100
      }
    })

    const { data } = await response.json()

    return ctx.render(data)
  }
}

That's it! You can now start both the Fresh and Grafbase dev servers!

npx grafbase@latest dev
deno task start

You will want to push your code to GitHub and connect the repository to Grafbase. Grafbase will then automatically detect the grafbase/schema.graphql and deploy a serverless backend in seconds!

You will want to use Deno Deploy to launch your Fresh app to production!

Build a Fresh backend with GraphQL

Deploy your Fresh backend in minutes with Grafbase.