DEV Community

Cover image for How to use ORMs (Prisma / Drizzle / Knex.js) in a TypeScript backend built with Encore.ts
Simon Johansson for Encore

Posted on

How to use ORMs (Prisma / Drizzle / Knex.js) in a TypeScript backend built with Encore.ts

In this post, I’ll walk you through how to work with ORMs like Prisma, Drizzle and Knex.js when working with Encore.ts.

We will look at how to create and manage Databases when running your application locally but also how to go about getting an app using databases deployed to the cloud.

Video version:

Using a Database with Encore

Encore treats databases as logical resources in your code and natively supports PostgreSQL databases. But let’s dive right into some code because that will speak for itself.

import { SQLDatabase } from "encore.dev/storage/sqldb";

const db = new SQLDatabase("url", { migrations: "./migrations" });
Enter fullscreen mode Exit fullscreen mode

To create a database with Encore, import SQLDatabase from Encore’s encore.dev/storage/sqldb module and call new SQLDatabase, giving it a name and a path to our migrations folder. In the migrations folder Encore expects ordinary SQL files, which is how you define the database schema.

Encore automatically handles up migrations. Each up migration runs sequentially, expressing changes in the database schema from the previous migration. The first migration typically defines the initial table structure. For instance, the first migration file for a URL shortener service might look like this:

1_create_table.up.sql

CREATE TABLE url (
    id TEXT PRIMARY KEY,
    original_url TEXT NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

This is how our folder structure could look like with multiple migration files:

/my-app
├── encore.app                       // ... other top-level files
│
└── url                              // url shortener service
    ├── migrations                   // database migrations (directory)
    │   ├── 1_create_table.up.sql    // first migration file
    │   └── 2_add_field.up.sql       // second migration file
    ├── url.ts                       // url service code
    └── url.test.ts                  // tests for url service
Enter fullscreen mode Exit fullscreen mode

With this code in place, Encore will automatically create the database using Docker when you run the command encore run locally.

We can now start querying and inserting data into the database. With the exec method we can write SQL code for inserting data.

import { api } from "encore.dev/api";
import { randomBytes } from "node:crypto";

interface URL {
  id: string; // short-form URL id
  url: string; // complete URL, in long form
}

interface ShortenParams {
  url: string; // the URL to shorten
}

// shorten shortens a URL.
export const shorten = api(
  { expose: true, auth: false, method: "POST", path: "/url" },
  async ({ url }: ShortenParams): Promise<URL> => {
    const id = randomBytes(6).toString("base64url");
    await db.exec`
        INSERT INTO url (id, original_url)
        VALUES (${id}, ${url})
    `;
    return { id, url };
  },
);
Enter fullscreen mode Exit fullscreen mode

We can even make use of template literals when writing our SQL query, allowing easy use of placeholder parameters. Encore takes care of the escaping here so there is no risk of SQL injections.

The get endpoint takes a user ID as input. We are using the  queryRow method, which returns a single row. We again use a template literal to send in the ID. If no rows are found we return an error, otherwise we get the original_url from the row.

import { api, APIError } from "encore.dev/api";

// Get retrieves the original URL for the id.
export const get = api(
  { expose: true, auth: false, method: "GET", path: "/url/:id" },
  async ({ id }: { id: string }): Promise<URL> => {
    const row = await db.queryRow`
        SELECT original_url
        FROM url
        WHERE id = ${id}
    `;
    if (!row) throw APIError.notFound("url not found");
    return { id, url: row.original_url };
  },
);

Enter fullscreen mode Exit fullscreen mode

Encore CLI comes with tools for you to interact with the locally running database. You can for example run encore db shell to open a psql shell to the database.

When you start your app using encore run you get access to the Local Development Dashboard. From here you can easily call your endpoints. Each call to your application results in a trace that you can inspect to see the API requests but also the database calls.

ORM

And if you don’t want to write SQL code by hand you can use ORMs like Prisma, Drizzle, or Knex if you prefer.

An ORM stands for Object–relational mapping, what this basically means is that we are using the power of object oriented programming to define a layer on top of our database.

An ORM abstracts away the communication with the database. It can speed up development and one nice thing is that you get type-safety when working with your database.

But using an ORM is not a given and you should think about if it suites your project because it can be really hard to move away from an ORM once you have committed.

But with that said, let’s look at how to use Prisma, one of the most popular ORMs for Node.js, together with Encore.

Prisma

Let’s take a look at how our schema.prisma could look like when integrating with Encore.

generator client {
  provider = "prisma-client-js"
  binaryTargets = ["native", "debian-openssl-3.0.x"]
}

datasource db {
  provider = "postgresql"
  url      = "<paste shadow db connection string here>"

}

model User {
  id      Int      @id @default(autoincrement())
  name    String
  surname String
  age Int
}
Enter fullscreen mode Exit fullscreen mode

For the datasource we need to set the provider and the url to the shadow database. Prisma requires a "shadow database" for certain operations, essentially it's a second, temporary, database that is created and deleted automatically. Encore comes with built-in support for shadow databases, and by configuring Prisma to operate on Encore's shadow database, Encore.ts and Prisma won't interfere with each other.

To get the Encore shadow URI we run the following command encore db conn-uri encore_prisma_test --shadow in the terminal. Then we paste that value into the schema file. It will look something like this: "postgresql://prisma-3wv2:shadow-csub8kpuevghlr9vvci0@127.0.0.1:9500/encore_prisma_test?sslmode=disable".

Now we are ready to create a database and a PrismaClient:

import { SQLDatabase } from "encore.dev/storage/sqldb";
import { PrismaClient } from "@prisma/client";

// Define a database named 'encore_prisma_test', using the database migrations
// in the "./prisma/migrations" folder (where prisma will generate their migrations).
// Set `source` to `prisma` to let Encore know that the migrations are generated by Prisma.
const DB = new SQLDatabase('encore_prisma_test', {
  migrations: {
    path: './prisma/migrations',
    source: 'prisma',
  },
});

// Setup prisma client with connection string
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: DB.connectionString,
    },
  },
});

// Select all users
const allUsers = prisma.user.findMany();
Enter fullscreen mode Exit fullscreen mode

We create a database like we did earlier, but this time we get the connectionString and pass that in when creating the PrismaClient. Now we can use the prisma object to query and update data in the database.

Run npx prisma migrate dev to create new migrations if you have made any changes to the schema.prisma file. The migration files will be automatically applied by Encore.

We also add the following NPM script that will run after installing our dependencies so that Prisma sets everything up in our CI pipeline when deploying:

{
  "scripts": {
    "postinstall": "npx prisma generate --schema=users/prisma/schema.prisma"
  },
}
Enter fullscreen mode Exit fullscreen mode

Drizzle

Let’s take a look at another popular ORM, Drizzle. In the database.ts file we again create a database and pass in the connectionString to drizzle:

// database.ts
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { drizzle } from "drizzle-orm/node-postgres";
import { users } from "./schema";

// Create SQLDatabase instance with migrations configuration
const db = new SQLDatabase("test", {
  migrations: {
    path: "migrations",
    source: "drizzle",
  },
});

// Initialize Drizzle ORM with the connection string
const orm = drizzle(db.connectionString);

// Query all users
await orm.select().from(users);
Enter fullscreen mode Exit fullscreen mode

In the Drizzle config we specify the migrations folder, our schema file and that we are using a PostgreSQL database:

// drizzle.config.ts
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: 'migrations',
  schema: 'schema.ts',
  dialect: 'postgresql',
});
Enter fullscreen mode Exit fullscreen mode

And this is what a Drizzle schema file looks like:

// schema.ts
import * as p from "drizzle-orm/pg-core";

export const users = p.pgTable("users", {
  id: p.serial().primaryKey(),
  name: p.text(),
  email: p.text().unique(),
});
Enter fullscreen mode Exit fullscreen mode

Run the npx drizzle-kit generate command to generate migrations. Migrations are automatically applied when you run your Encore application, so you don’t need to run drizzle-kit migrate or any similar commands manually.

Knex

Knex is a “SQL query builder”, which is somewhere in between an ORM and writing raw SQL queries:

// site.ts
import { SQLDatabase } from "encore.dev/storage/sqldb";
import knex from "knex";

// Create SQLDatabase instance with migrations configuration
const SiteDB = new SQLDatabase("siteDB", {
  migrations: "./migrations",
});

// Initialize Knex with the database connection string
const knexDB = knex({
  client: "pg",
  connection: SiteDB.connectionString,
});

// Define the Site interface
export interface Site {
  id: number;
  url: string;
}

// Query builder for the "site" table
const Sites = () => knexDB<Site>("site");

// Example queries

// Query all sites
await Sites().select();

// Query a site by id
await Sites().where("id", id).first();

// Insert a new site
await Sites().insert({ url: params.url });
Enter fullscreen mode Exit fullscreen mode

We create a database like we did earlier, and pass in the connectionString to the database when creating a new Knex client. We also need to specify that we are using PostgreSQL. Then we can create a query builder by using the knexDB object, passing in the type of the object we are storing (Site) and the table name ("site").

Now we can use the Sites object to query and update data in the database.

A query builder is easier to set up but you don’t get the same level of autocompletion in your editor as with an ORM.

Deploy

So, how do we deploy this application? Well you can build your application using encore build docker image, and you get it as a docker image you can deploy anywhere you want. You will need to supply a runtime configuration where you can specify how the application should connect to the infrastructure you are using, like databases and Pub/Sub. If you don’t feel like managing this stuff manually, you can use Encore's Cloud Platform which automates setting up the needed infrastructure in your cloud account on AWS or GCP, and it comes with built-in CI/CD so you just need to push to deploy. In cloud environments, Encore automatically injects the appropriate configuration to authenticate and connect to the database, so once the application starts up the database is ready to be used. The Platform also comes with monitoring, tracing, and automatic preview environments so you can test each pull request in a dedicated temporary environment.

Running the examples yourself

If you want to play around with this application yourself you can easily do so by installing Encore and then clone an example from our examples repo. You will need to have Docker desktop installed as that is needed to create databases locally.

Installation

macOS: brew install encoredev/tap/encore
Linux: curl -L https://encore.dev/install.sh | bash
Windows: iwr https://encore.dev/install.ps1 | iex

Cloning the ORM examples

Prisma

encore app create --example=ts/prisma
Enter fullscreen mode Exit fullscreen mode

Drizzle

encore app create --example=ts/drizzle
Enter fullscreen mode Exit fullscreen mode

Knex.js

encore app create --example=ts/knex
Enter fullscreen mode Exit fullscreen mode

Wrapping up

  • ⭐️ Support Encore by giving the project a star on GitHub.

  • If you have questions or want to share your work, join the developers hangout in Encore's community on Discord.

Other related posts

Top comments (0)