Adding comments to my Astro blog with PlanetScale & Prisma on Vercel Edge
Tags: database, html, astro, javascript
Published: 2022-11-28 18:11:00

After I created my [blog platform using Astro](https://www.thomasledoux.be/blog/create-blog-astro-mdx), the next thing on my list was to make it possible for people to (anonymously) leave comments on blog posts.
For this I needed to add a database to my architecture.

## Choice of technologies

I chose [PlanetScale](https://wwww.planetscale.com) because it's serverless and MySQL, 2 of my criteria.
To communicate with my PlanetScale database, I chose to use [Prisma](https://www.prisma.io/), a Node.js and TypeScript ORM.

## Set up Prisma

I started by adding the Prisma client: `npm install prisma @prisma/client`.
After this install, I added my Prisma schema to my codebase:

```prisma
// schema.prisma

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["referentialIntegrity"]
}

datasource db {
  provider     = "mysql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"
}

model Post {
  id         Int       @id @default(autoincrement())
  createdAt  DateTime  @default(now())
  url        String
  like_count Int @default(0)
  Comment    Comment[]
}

model Comment {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  text      String
  author    String
  postId    Int
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
}
```

Then it got a bit tricky.
Because I'm deploying my blog platform on Vercel's Edge network, the connections from the platform to the database can [not have a persistent connection](https://vercel.com/guides/using-databases-with-vercel#connecting-to-your-database).
After some research, I found out Prisma offers a service to set up connection pooling called `Prisma Data Platform`.
Once I had created an account on the Data Platform, I was able to create a `Data Proxy`, which provided me with a connection string to use in my application.
This connection string is what I needed to put in the `DATABASE_URL` environment variable (which is used in the `prisma.schema`).

To generate the TypeScript types based on my Prisma schema, I just ran `npx prisma generate`, this will by default generate the types in the `node_modules` folder in your project locally.

## Setting up PlanetScale

Syncing the schema from Prisma to PlanetScale is as easy as running `npx prisma db push` in your terminal.

## Creating comments

To communicate from my frontend to my API routes, I opted to use [TanStack Query](https://tanstack.com/query/v4), my favourite tool to handle client-side API calls in React.
The frontend code to add and list comments for a blog post looks like this (this part I wrote in React):

```tsx
import type { Comment } from '@prisma/client';
import { useQuery } from '@tanstack/react-query';
import { useRef, useState, Fragment } from 'react';
const CommentsUpvotes = ({
  initialComments,
  blogUrl,
}: {
  initialComments?: Comment[];
  blogUrl: string;
}) => {
  const formRef = useRef<HTMLFormElement>(null);
  const [formState, setFormState] = useState<'idle' | 'loading'>('idle');
  const upToDateCommentsQuery = useQuery({
    queryKey: [`comments-${blogUrl}`],
    queryFn: async () => {
      const allCommentsInDb = await fetch(
        `/api/comments/list?blogUrl=${blogUrl}`
      );
      const allCommentsInDbJson = await allCommentsInDb.json();
      return allCommentsInDbJson as Comment[];
    },
    initialData: initialComments,
  });
  const onSubmit = async (e: React.FormEvent) => {
    setFormState('loading');
    e.preventDefault();
    if (e.currentTarget) {
      const formData = new FormData(e.currentTarget as HTMLFormElement);
      formData.set('blogUrl', blogUrl);
      await fetch('/api/comments/create', {
        method: 'POST',
        body: formData,
      });
      formRef.current?.reset();
      upToDateCommentsQuery.refetch();
    }
    setFormState('idle');
  };
  return (
    <Fragment>
      <h2 className="text-xl lg:text-2xl mb-4 font-bold">Add a comment</h2>
      <form
        onSubmit={onSubmit}
        ref={formRef}
        className="flex flex-col lg:w-[50%] items-start"
      >
        <label className="flex flex-col mb-2" htmlFor="author">
          Author
        </label>
        <input
          className="py-2 px-4 bg-white border-secondary border-2 rounded-lg w-full"
          placeholder="Author"
          name="author"
          required
        />
        <label className="flex flex-col mb-2 mt-4" htmlFor="comment">
          Comment
        </label>
        <textarea
          className="py-2 px-4 bg-white border-secondary border-2 rounded-lg w-full"
          placeholder="Comment"
          required
          rows={4}
          name="comment"
        ></textarea>
        <button
          disabled={formState === 'loading'}
          className="px-8 mt-4 py-4 bg-secondary text-white rounded-lg lg:hover:scale-[1.04] transition-transform disabled:opacity-50 "
          type="submit"
        >
          {formState === 'loading' ? 'Submitting' : 'Submit comment'}
        </button>
      </form>

      <h2 className="text-xl lg:text-2xl mb-4 font-bold">Comments</h2>
      {upToDateCommentsQuery?.data && upToDateCommentsQuery?.data.length > 0 ? (
        <div className="flex flex-col gap-y-4">
          {upToDateCommentsQuery?.data?.map(comment => (
            <div key={comment.id} className="flex flex-col">
              <h3 className="font-bold">{comment.author}</h3>
              <div>{comment.text}</div>
            </div>
          ))}
        </div>
      ) : (
        <div className="mt-4">No comments yet. Be the first to add one!</div>
      )}
    </Fragment>
  );
};

export default CommentsUpvotes;
```

The code for the API route to create blogs looks like this:

```typescript
import type { APIRoute } from 'astro';
import { prisma } from '../../../lib/prisma-client';

export const post: APIRoute = async ({ request }) => {
  const formData = await request.formData();
  const comment = formData.get('comment') ?? '';
  const author = formData.get('author') ?? '';
  const blogUrl = formData.get('blogUrl') ?? '';
  const blog = await prisma.post.findFirst({
    where: { url: blogUrl as string },
  });
  if (!blog) {
    await prisma.post.create({
      data: {
        url: blogUrl as string,
        Comment: {
          create: {
            author: author as string,
            text: comment as string,
          },
        },
      },
    });
  } else {
    await prisma.comment.create({
      data: {
        author: author as string,
        text: comment as string,
        postId: blog.id,
      },
    });
  }

  return new Response(null, {
    status: 200,
  });
};
```

And the code to list the comments for a post looks like this:

```typescript
import type { APIRoute } from 'astro';
import { getCommentsForBlog } from '../../../lib/get-comments-for-blog';

export const get: APIRoute = async ({ request }) => {
  const params = new URLSearchParams(request.url.split('?')[1]);
  const allCommentsInDbForPost = await getCommentsForBlog(
    params.get('blogUrl')
  );
  return new Response(JSON.stringify(allCommentsInDbForPost), {
    status: 200,
  });
};
```

## Setting up the 'Edge' part

Deploying Astro to Vercel Edge is as easy as adding the [Astro with Vercel integration](https://docs.astro.build/en/guides/integrations-guide/vercel/) and setting up the edge `target`.
My astro config (note the `edge` in the import path):

```js
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/edge';

export default defineConfig({
  output: 'server',
  adapter: vercel(),
});
```

I also had to configure an [alias in Vite]() for the Prisma client to get it working on Vercel combined with Astro.

```js
// vite.config.js
export default {
  resolve: {
    alias: {
      '.prisma/client/edge': './node_modules/.prisma/client/edge.js',
    },
  },
};
```

When building the application on Vercel, we also want to [generate the client](https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/generating-prisma-client) again to make sure the client is available in the `node_modules` there too.
In the `package.json` I use `"build": "prisma generate --data-proxy && astro build"` for this.
Instantiating the Prisma client in the code is done in `lib/prisma-client.ts`.
I used lazy imports from Node.js here to make it work correctly locally and on Vercel.

```typescript
import type { PrismaClient } from '@prisma/client';

let prisma: PrismaClient;

if (process.env.NODE_ENV === 'development') {
  import('@prisma/client').then(mod => (prisma = new mod.PrismaClient()));
} else {
  import('@prisma/client/edge').then(mod => (prisma = new mod.PrismaClient()));
}
export { prisma };
```

To make my code run locally too, I needed to change the `DATABASE_URL` environment variable to make it point directly to PlanetScale instead of going through Prisma Proxy.<br></br>
Go check it out on [my blog](https://www.thomasledoux.be/blog), and add a comment ;-).
Source code can be found on [my Github](https://github.com/thomasledoux1/website-thomas-astro).