A blog by Jeffrey | (@jefiozie)

How to build a JamStack app with Angular Scully and Prisma

TLDR;

By using Angular Scully and Prisma we have a powerful ecosystem where we can leverage full type safety and easily connect to a database that contains data that will be injected into the components by Scully. After the Scully process, we can upload our app to a CDN and have a fully working SEO-friendly Angular app.

What is Prisma?

Prisma is an open-source ORM, it consists of three parts:

  • Prisma Client: Auto-generated and type-safe query builder for Node.js & TypeScript
  • Prisma Migrate: Migration system
  • Prisma Studio: GUI to view and edit data in your database

Prisma Client can be used in any Node.js (supported versions) environment. The power of Prisma is that they have a good and powerful type safety solution when using it with Typescript, and by default supports a set of Easy to use querIes.

What is Scully

Scully is the static site generator for Angular projects looking to embrace the Jamstack.

It will use your application and will create a static index.html for each of your pages/routes. Every index.html will have the content already there, and this will make your application show instantly for the user. Also, this will make your application very SEO-friendly. On top of this, your SPA will still function as it did before.

How to set up an Angular app with Scully?

In this article, we are not going very deep into the setup of Angular and Scully. For a good reference, you can have a look at the repo. Below are the steps you can follow along:

First, let’s set up a new Angular app

npx ng new ng-scully --minimal

Second, let’s add Scully to the Angular application

ng add @scullyio/init

Scully schematics will do the following:

  • Add Scully dependencies to package.json and install it
  • Import ScullyLibModule to AppModule
  • Add 'zone.js/dist/task-tracking' to polyfills.ts
  • Add scully.<project_name>.config.ts to the root directory. This is Scully configuration file that we will utilize to configure Scully.

Now we have a setup that works with Angular, but we need to take one step more for this demo.

ng generate @scullyio/init:blog

The above command adds the blog modules' routes to the Angular application. In addition, it creates a ./blog folder for the blog's markdown files.

How to use Prisma with Scully?

I’ve made the choice to use postgress as my database, in combination with docker.

Below I’m showing you my docker-compose file:

version: '3'
services:
 postgres:
    image: postgres
    ports:
      - "5432:5432"
    restart: always
    environment:
      POSTGRES_USER: prisma
      POSTGRES_PASSWORD: prisma
    volumes:
      - postgres:/var/lib/postgresql/data
volumes:
  postgres:

Now we only need to run it so that Prisma can connect to it.

docker-compose up -d

Now we can continue with Prisma, first we need to install Prisma

npm install prisma --save-dev

After installation we will run the init command as shown below:

npx prisma init

This init command will setup Prisma with the required files in the directory.

After this we need to change the .env file with our database connection:

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

Setup Prisma configuration

To use Prisma with Scully we first need to add the setup for Prisma.

As Prisma is an ORM for a database we need to tell Prisma what tables and/ or database it is connected to. This information is placed in the schema.prisma file:

// This is your Prisma schema file,

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}


model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User?   @relation(fields: [authorId], references: [id])
  authorId  Int?
}

model User {
  id        Int      @id @default(autoincrement())
  name      String?
  email     String?  @unique
  createdAt DateTime @default(now()) @map(name: "created_at")
  updatedAt DateTime @updatedAt @map(name: "updated_at")
  posts     Post[]

  @@map(name: "users")
}

Note: You're occasionally using @mapand@@mapto to transform some field and model names to different column and table names in the underlying database.

This Prisma schema defines two models, each of which will map to a table in the underlying database: User and Post. Notice that there's also a relation (one-to-many) between the two models, via the author field on Post and the posts field on User.

Now that we have our schema defined, we need to create our tables in our database. This can be done by running the following CLI command:

npx prisma db push

You should see the following output:

Environment variables loaded from .env
Prisma schema loaded from prisma\schema.prisma
Datasource "db": PostgreSQL database "mydb", schema "public" at "localhost:5432"
The database is already in sync with the Prisma schema.
βœ” Generated Prisma Client (3.8.1 | library) to .\node_modules\@prisma\client in
75ms

As our database is ready to be used, let’s add some data. We are going to use Prisma Studio, this is an easy way to explore and manipulate the data. You can open up Prisma Studio by running

npx prisma studio

Create a Prisma Scully Plugin

As we now have an operating database and an ORM library (Prisma) we now can use all these parts to receive the data and use it in Scully. Let’s start by creating the first basis for our custom plugin.

import { PrismaClient } from "@prisma/client";
import {
  HandledRoute,
  logError,
  registerPlugin,
  RouteConfig,
} from "@scullyio/scully";
import { convertAndInjectContent } from "@scullyio/scully/src/lib/renderPlugins/content-render-utils/convertAndInjectContent";
export const prismaPlugin = "prismaPlugin";
const prisma = new PrismaClient();
const routerPlugin = async (route: string, config: RouteConfig) => {
  // here we are looking up all posts
  const data = await prisma.post.findMany({
    // where the published property is true
    where: { published: true },
    // and we include the author
    include: {
      author: {
        // and we only want the author's name
        select: { name: true },
      },
    },
  });
  return Promise.resolve(
    // let's loop over all posts
    data.map((post) => {
      // and return a new route for each post
      const { id, title, published, author, content } = post;
      return {
        ...config,
        route: `/blog/${id}`,
        data: {
          id,
          title,
          published,
          author: author.name,
          content,
        },
      } as HandledRoute;
    })
  );
};
registerPlugin("router", prismaPlugin, routerPlugin);

async function prismaDomPlugin(dom: any, route: HandledRoute | undefined) {
  if (!route) return dom;
  try {
    try {
      // here we use the power of scully and use the filehandler to convert the content to html
      return convertAndInjectContent(dom, route.data.content, "md", route);
    } catch (e) {
      logError(`Error during contentText rendering`);
      console.error(e);
    }
    return dom;
  } catch (e) {}
}

registerPlugin("postProcessByDom", prismaPlugin, prismaDomPlugin);

Let’s break this code down from the top.

import { PrismaClient } from '@prisma/client';
import { logError, registerPlugin, RouteConfig } from '@scullyio/scully';

//define our plugin name
export const prismaPlugin = 'prismaPlugin';

// setup our PrismaClient
const prisma = new PrismaClient();

// our router plugin
const routerPlugin = async (route: string, config: RouteConfig) => {
...
};

Now we are going to retrieve the posts with the Prisma client. When all the data is gathered we will return new routes that will be used on our post-render step.

const prisma = new PrismaClient();
const routerPlugin = async (route: string, config: RouteConfig) => {
  // here we are looking up all posts
  const data = await prisma.post.findMany({
    // where the published property is true
    where: { published: true },
    // and we include the author
    include: {
      author: {
        // and we only want the author's name
        select: { name: true },
      },
    },
  });
  return Promise.resolve(
    // let's loop over all posts
    data.map((post) => {
      // and return a new route for each post
      const { id, title, published, author, content } = post;
      return {
        ...config,
        route: `/blog/${id}`,
        data: {
          id,
          title,
          published,
          author: author.name,
          content,
        },
      } as HandledRoute;
    })
  );
};

The post-process plugin is used to transform the render HTML. In our custom plugin, we make use of the Scully system, the converAndInjectContent function will look at the fileHandler plugins, and if it finds an extension of a file type. In our case, it will look for the fileHandler for markdown files. This plugin will transform our data coming from the database from markdown to HTML.

async function prismaDomPlugin(dom: any, route: HandledRoute | undefined) {
  if (!route) return dom;
  try {
    try {
      // here we use the power of scully and use the filehandler to convert the content to html
      return convertAndInjectContent(dom, route.data.content, "md", route);
    } catch (e) {
      logError(`Error during contentText rendering`);
      console.error(e);
    }
    return dom;
  } catch (e) {}
}

Now that we have set up our plugin, we need to make one new change to our Scully configuration. We need to change the original blog route to use our custom plugin, first, we need to import our custom plugin

import { prismaPlugin } from "./scully/plugins/plugin";

Then we need to define our router and post process plugin to being used in our blog route.

routes: {
    "/blog/:slug": {
      type: prismaPlugin,
    }
  }

Finally, we are ready to run our Scully system to scan for new routes, run npx scully --scan

$ npx scully --scan
  βœ” new Angular build files imported
  βœ” Starting servers for project "asp-example"
  βœ” Started Angular distribution server on "http://localhost:1864/" 
  βœ” Started Scully static server on "http://localhost:1668/"
  βœ” Scully Development Server is up and running
  βœ” Puppeteer is being launched
  βœ” Successfully scanned Angular app for routes
  βœ” Successfully added routes created from routePlugins
  βœ” Route list created in files:
     ".\src\assets\scully-routes.json",
     "dist\static\assets\scully-routes.json",
     "dist\asp-example\assets\scully-routes.json"

  βœ” Route "/blog" rendered into ".\dist\static\blog\index.html" 
  βœ” Route "/home" rendered into ".\dist\static\home\index.html" 
  βœ” Route "/" rendered into ".\dist\static\index.html" 
  βœ” Route "/blog/1" rendered into ".\dist\static\blog\1\index.html" 

Total time used 5.74 seconds
  4 pages have been created
  Rendering the pages took 2.99 seconds
  That is 1.34 pages per second,
  or 749 milliseconds for each page.
  
  Finding routes in the angular app took 2.68 seconds
  Pulling in route-data took 47 milliseconds

We now have our first page rendered with Angular, Scully, and Prisma.

Conclusion

With Prisma we have a powerful type of safety solution to connect to a database, combine this with the power of Scully we can easily create static pages from an Angular application and upload it to a CDN.