NextJS with Prisma on Kubernetes

deploy as a knative service

Published December 8, 2021 #nextjs, #javascript, #prisma, #knative, #kubernetes

Now that we have our cluster up and running, lets look at how to build and deploy a NextJS app on it, including the database.

Create a NextJS app

We'll scaffold out a TypeScript app.

  npx create-next-app@latest --typescript  myapp 

  cd myapp

  npm run dev

Fireup a local data

  docker run -e POSTGRES_PASSWORD=awesome_password -p 5432:5432 postgres

Install prisma

We'll add the npm packages to our project.

  npm install prisma

  npx prisma init

Edit .env and change the url to be

  DATABASE_URL="postgresql://postgres:awesome_password@localhost:5432/mydb?schema=public"

Data Models

In primsa/schema.prisma add a model:

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  title     String   @db.VarChar(255)
  content   String?
}

Run the migration

  npx prisma migrate dev --name init

Run prisma studio to check it out

  npx prisma studio

Write the post page and api

This is a basic demostration, so nothing fancy.

tsconfig.json

Add

"baseUrl" : "." and "strict":"false" to compilerOptions.

lib/prisma.ts

  import { PrismaClient } from '@prisma/client';

  declare global {
    var prisma: PrismaClient | undefined;
  }

  export const prisma = global.prisma || new PrismaClient();

  if (process.env.NODE_ENV !== 'production') global.prisma = prisma;

pages/posts.ts

We'll use react-hook-form to make things simplier.

  npm install react-hook-form

A couple points.

  1. We query the database in getServiceSideProps

  2. We are fetching our API from window.location.origin

  3. We are totally hacking the window reload, I'm just lazy

  import Head from 'next/head';
  import { prisma } from 'lib/prisma';
  import { Post } from '.prisma/client';
  import Link from 'next/link';
  import { useForm } from 'react-hook-form';

  export default function PostsPage({ posts }) {
    const {
      register,
      handleSubmit,
      formState: { errors, isSubmitted },
    } = useForm();

    const onSubmit = (data) => {
      try {
        fetch(`${window.location.origin}/api/create`, {
          body: JSON.stringify(data),
          headers: {
            'Content-Type': 'application/json',
          },
          method: 'POST',
        }).then( () => {
          window.location.href = window.location.href
         } )
      } catch (error) {
        throw new Error(error);
      }
    };

      return (
        <>
          <ul>
              {posts.map((item: Post) => (
                  <li key={item.id}>{item.title} - {item.content}</li>
              ))}
          </ul>
          <form onSubmit={handleSubmit(onSubmit)}>
            <label>Title</label>
            <input 
              id="title"
              type="text" 
              {...register('title', {required: true})}/>
              <br/>
            <label>Content</label>
            <input 
              type="text" 
              id="content"
              {...register('content', {required: true})}/>
            <input type="submit"/>
          </form>
          </>
      );
  }

  export const getServerSideProps = async () => {
    const posts = await prisma.post.findMany({
      select: {
        id: true,
        title: true,
        content: true
      },
    });

    console.log(posts);
    return {
      props: {
        posts,
      },
    };
  };

api/create.ts

This is the api request to handle the post. It runs on the server.

import { prisma } from 'lib/prisma';

export default async function handler(req, res) {
  const { content, title } = req.body;

  try {
    const feedback = await prisma.post.create({
      data: {
        content,
        title,
      },
    });
    res.status(200).json(feedback);
  } catch (error) {
    res.status(400).json({
      message: `Something went wrong :/ ${error}`,
    });
  }
}

Setup postgres on your cluster

  helm repo add bitnami https://charts.bitnami.com/bitnami
  helm upgrade --install postgres bitnami/postgresql

Eventually the database will be available on postgres-postgresql.default.svc.cluster.local

We can get the password using

  kubectl get secret --namespace default postgres-postgresql -o jsonpath="{.data.postgresql-password}" | base64 --decode

When we deploy the function we'll set the DATABASE_URL to look something like:

  DATABASE_URL="postgresql://postgres:1oOFIcOvB1@postgres-postgresql.default.svc.cluster.local:5432/database?schema=public"

You'll need to update that with your actual password of course.

Migrate the database

We are going to setup the database from our local enviornment. We'll use kubectl to forward the local postgres port to the remote install. Remember to stop your local postgres container

  kubectl port-forward svc/postgres-postgresql 5432:5432

Then, using the password that we found above, deploy the current migrations to the postgres that's being forwared on localhost. This is a different url then the previous one shown!:

  DATABASE_URL="postgresql://postgres:1oOFIcOvB1@localhost:5432/database?schema=public" \
              npx prisma migrate deploy

You can also run prisma studio that way, by the way.

Deploy the service

Build image

We need to tweaks the dockerfile from the official docs to add prisma generate to get the client code. So, copy over that Dockerfile and then, right after it copies the node_modules over added

RUN npx prisma generate

For me it's on like 14, right before yarn build.

Since I'm working off an M1 mac, I need to set the --platform to make sure that the image is built using the correct binaries. An example of this is:

  docker build . --platform linux/amd64 -t wschenk/prismatest && docker push wschenk/prismatest

Create the service

    kn service create prisma \
       --image wschenk/prismatest \
       --env DATABASE_URL="postgresql://postgres:1oOFIcOvB1@postgres-postgresql.default.svc.cluster.local:5432/database?schema=public"

Once this is up and running, you should be able to interact with your service and add messages.

References

  1. https://github.com/m-abdelwahab/nextjs-prisma-workshop

  2. https://nextjs.org/docs/deployment

Read next

See also

Serving a knative function on the root

root to services

I want to deploy everything as a knative service, including the root of the domain. Update: I found an easyier way. Easy way Turn on auto-tls and autocreate-cluster-domain-claims: kubectl patch configmap config-network --namespace knative-serving -p '{"data":{"auto-tls":"Enabled","autocreate-cluster-domain-claims":"true"}}' Then kn domain create gitgratitude.com --ref=homepage That's it. Hardway Left here for the record. Add ingress-nginx helm upgrade --install ingress-nginx ingress-nginx \ --repo https://kubernetes.

Read more

Logging with an HTTP Proxy

Simple MITM debugging

I miss having Chrome dev tools when writing node applications, since I often want to see what exactly the network traffic. Here's a way to easily setup a man in the middle proxy to look at all the traffic your application is generating. Run the docker instance First we first up the proxy with the web interface: docker run --rm -it -p 8080:8080 -p 8081:8081 mitmproxy/mitmproxy mitmweb --web-host 0.

Read more

Simple CORS workaround for local development

I've been doing a lot of web development old school, just editing HTML and JavaScript by hand without a build environment. Running npx live-server is an easy one liner to spin up a server, that opens a browser for you and also updates changes on safe. Sometimes that's not enough. Often you want to pull in data from an API, and that requires HTTPS, and also you need to work around CORS limitations.

Read more