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!