Set up a Prisma and PostgreSQL back-end environment with Docker and Typescript

This article is meant to explain how to quickly set up from scratch a Nodejs application running with Prisma and PostgreSQL in a Docker environment, using docker-compose.

Recently I was looking for an ORM I could use with my Nodejs application and my PostgreSQL database. Wasn't I glad to discover Prisma, a new kind of ORM as it's described on their website. The documentation is very clear, whether you have your project running with Typescript or not. But I thought I would write, as for myself and also for potential beginners, a little guide to set it all up through docker.


At this end of this guide, you should have your complete back-end environment (nodejs/expressjs, typescript, postgresql, prisma) running into a container. Sources are available on my github as well, feel free to use it!

Obviously, there are some requirements to follow this guide: you need to have npm, node, docker and docker-compose installed on your local machine, as well as some previous experience toying around with these tools.



Set up the nodejs server

Let's start a new project and initiate it with npm init -y, add expressjs in the dependencies and our dev dependencies for Typescript.

bash
npm i express
npm i -D nodemon ts-node typescript @types/express @types/node @types

First we take care of our tsconfig.json file at the root of our project directory, you may have a different configuration but here's mine:

json
{
"compilerOptions": {
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"outDir": "./dist" /* Redirect output structure to the directory. */,
"strict": true /* Enable all strict type-checking options. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

We are also using nodemon for the hot reload, so we need to configure it for Typescript as well. At the root of your project, create a nodemon.json file with the following content:

json
{
"restartable": "rs",
"ignore": [".git", "node_modules/", "dist/", "coverage/"],
"watch": ["src/"],
"execMap": {
"ts": "node -r ts-node/register"
},
"ext": "js,json,ts"
}

Add this line in the scripts of your package.json for the hot reload:

json
"scripts": {
"dev": "nodemon --config nodemon.json src/server.ts",
}

We're keeping it simple here and make a very basic server running, paste the following into a server.ts file into the src folder of your project.

typescript
// src/server.ts
import express from 'express'
const app = express()
const PORT = 5000
app.use(express.json())
app.get('/', (req, res) => {
res.json('hello there')
})
app.listen(PORT, () => {
console.log(`listening on port ${PORT}`)
})

If you made it up to this point, everything should be working like a charm 👌! You can of course test it by typing npm run dev and try to access the only route in browser.



Add our database

Our server doesn't do a lot for sure, but it would be nice to connect it to our PostgreSQL database. You may wonder why this choice over MySQL, but it won't be discussed here as there are plenty resources on the Internet for you to find out!


Dockerize the server

First thing first, we have to write a Dockerfile for the server. We basically fetch a light version of node, install our dependencies in the working directory, then run the server.

Dockerfile
FROM node:alpine
# Create app directory
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5000
CMD [ "npm", "run", "dev" ]

Create a container for our database

In order for both our server and database to be able to communicate within the same container, we use docker-compose. Write the following into our docker-compose.yml file where we define a very basic user and database name.

You are obviously more than welcome to expose different ports, or change this configuration as you wish.

yml
version: '3.9'
services:
db:
image: 'postgres'
ports:
- '2345:5432'
environment:
POSTGRES_USER: 'postgres'
POSTGRES_PASSWORD: 'postgres'
POSTGRES_DB: 'mydb'
server:
build: .
ports:
- '5000:5000'
environment:
DATABASE_URL: 'postgresql://postgres:postgres@db:5432/mydb?schema=public'

Once again, if you made it up to this point, everything should be working like a charm 👌!

Just test your set up by simply running docker-composer up -d and you should see both your images running inside a prisma-docker container.



Connect our database with Prisma

Okay now is the time to connect and send data to our database! For that we use an ORM called Prisma. I won't be talking about how cool this could be, or how it works (though hopefully I will write one day about it). The goal here is to get you all set up for coding, and that's what we're about to do!

  • Let's install prisma in our dev dependencies by running npm i -D prisma
  • According to Prisma documentation we have to init prisma in order to add our schemas later: npx prisma init

That last command creates a repository (called prisma) at your root project containing a prisma.schema file. This is where we configure our provider (kind of databse), and the url to connect it. Usually this url is defined in the .env file of your project, but remember (!) we are working in a Docker container so our environement is already defined in our docker-compose file. We don't have anything else to do about it, yay.


Create models

This part is pretty straightforward too and for the sake of the example, we just add a simple user (with an email, a password and a name) as our only model:


// prisma.schema
model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
}

Install Prisma Client

Prisma Client is used to make query to our database, it is not a dev dependencies, so we install it through npm install @prisma/client.


Seed the databse

For those of you who might want to get started with a non-empty database, there is a simple way to do that consisting in creating a prisma/seed.ts file. And add the following example:

typescript
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const usersData = [ // we only have on user here
{
email: 'email@domain.com',
name: 'name'
}
]
const main = async () => {
console.log('start seeding …')
for (const _user of usersData) {
const user = await prisma.user.create({
data: _user,
});
console.log(`Created user with id: ${user.id}`);
}
console.log('seeding done');
}
main()
.catch(e => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

Test it all!

Before doing anything more, it is time to rebuild our image to make our changes effective (docker build for instance).

Let's then update our server.ts and add a friendly route to retrieve our users:

typescript
// src/server.ts
import { PrismaClient } from '@prisma/client' // don't forget to include these two lines!
const prisma = new PrismaClient();
// […]
app.get('/users', async (req, res) => {
try {
const users = await prisma.user.findMany()
res.json(users)
} catch(err) {
console.log(err)
}
})
// […]

Now we update our Dockerfile and add (before our final CMD line) this prisma command: RUN npx prisma generate used for generating a version of Prisma Client tailored to our model, build and run our container with docker-compose up -d again.


Feed the dockerized database for real

In order to feed our dockerized database and initiate prisma, first we enter the shell of our container and initialize it:

🧐 at the time I am writing this article, the current prisma version (2.21.2) is not compatible yet with the current last version (16) of nodejs. So you might want to downgrade and use node:15-alpine in your Dockerfile instead.

bash
docker-compose exec server /bin/sh
npx prisma migrate dev --name init && npx prisma db seed --preview-feature

And that's it!

You can now access your route localhost:5000/users and see the response by yourself. Keep in mind that it is totally normal to not have any prisma/migrate folder in your project directory, because this little fella is in your docker container.



Conclusion

You should now be able to enjoy your time and code! As a quick reminder, we achieved the following:

  • Set up an Express (with Typescript !) and PostgreSQL servers on a Docker container
  • Add Prisma into the mix

You can find the source code on my github repository: https://github.com/grdnmsz/prisma-docker This guide is meant to be quick and straightforward, please feel free to reach me out in you have any questions!

BONUS: For the most curious of you, I made a bigger project using this exact set up (except Typescript) for an interview exercise, it is available here: https://github.com/grdnmsz/unkle_api , so feel free to have a look!


Additionnal resources