Set up a Prisma and PostgreSQL back-end environment with Docker and Typescript
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.
bashnpm i expressnpm 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.tsimport express from 'express'const app = express()const PORT = 5000app.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.
DockerfileFROM node:alpine# Create app directoryWORKDIR /usr/src/appCOPY package*.json ./RUN npm installCOPY . .EXPOSE 5000CMD [ "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.
ymlversion: '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.schemamodel User {id Int @default(autoincrement()) @idemail String @uniquename 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.tsimport { 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.tsimport { 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.
bashdocker-compose exec server /bin/shnpx 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!