Prisma + Node + Typescript + Jest + MongoDB + TDD= ooOMG!! ⚡️

Bashar Saleh
11 min readJan 11, 2019

--

New way to architect your applications using Prisma as DAL layer

What do we want (we = js enthusiasts)?

We want to build software that easy to test, scalable, high performent .. etc using javascript and this requires patterns, tools, practices.

How to do that ❓ This article will answer that in modern way with simple example.

Introduction:

Let me introduce you my “single_article_apiproject. this project is just about single API to get articles /articles ,that’s it Tara!!!.

let’s begin

Create an empty folder name it “single_article_apiand initialize npm project inside it npm init -y .

Now let’s download the project packages

npm i -S express morgan typescript ts-node

npm i -D @types/express @types/node @types/morgan jest ts-jest @types/jest supertest

supertest is for sending http request during our test, jest ts-jest is for testing our typescript code.

Structuring our project:

Basic Folder Structure

Now to configure jest and ts-jest in our project we can create our config file manually or we can just run the command npx ts-jest config:init this will create jest.config.js .

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
}

to run our tests using jest, add this line to scripts field inside our package.json file "test": "jest" .

To configure typescript in our project properly, create tsconfig.jsonfile and add the following code to it:

{
"compilerOptions": {
"outDir": "dist",
"target": "es2015",
"module": "commonjs",
"resolveJsonModule": true,
"moduleResolution": "node",
"lib": ["es2015", "esnext.asynciterable"],
"sourceMap": true,
},
"exclude": ["node_modules"],
"include": ["*.ts"]
}

Now let the fun begin with Round 1:

How can we describe our only request to the server?. Simple, when we hit the /articles URL we should receive 200 Ok response. So let’s write our first acceptance test. First we create under test folder a new folder name it acceptance_tests.

Folder Structure

Then we create a test file with name articles.test.ts which contains our first acceptance test

import * as request from "supertest";describe("GET /articles", () => { it("SHOULD return 200Ok", done => {  request(app)
.get("/articles")
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});});

To run the test just run the command npm test test/acceptance_tests . It will fails with a message Cannot find name 'app' .

Now let’s make the test happy. We need to build our express app server by creating server.ts under src folder and adding this code to it :

import * as express from "express";
import * as morgan from "morgan";
const app = express();app.use(morgan("dev"));app.listen(3000, () => console.log("[SERVER] is up and running on 3000 ..."));export{app}

By the way morgan is just simple logger to see what’s happening to the server during http requests.

Now add import {app} from "../../src/server"; to the test file and rerun the test again and OOPS!! another failure but this time is different :

Expected value to equal:
200
Received:
404

That’s because we don’t have GET /articles handler, let’s create one inside server.ts :

import * as express from "express";
import * as morgan from "morgan";
const app = express();app.use(morgan("dev"));
app.get("/articles", (req, res) => {
res.json({}).status(200)
});
app.listen(3000, () => console.log("[SERVER] is up and running on 3000 ..."));export{app}

Rerun the test and Tara!! the green is shining, you pass the test.

Important Note:
What we have done is just a not-full Walking Skeleton test to let us know what are the major components of our project. Real Walking Skeleton must hit all the major components (end2end) including the DB but I have not done that because I want to introduce you to our new friend Prisma later on.

Round 2:

Now let’s create our second acceptance test next to our first one inside test/acceptance_tests/articles.test.ts that will hit all the major components of our project:

NOTE: how we structure this test is based on our Architecture view to the project.

it("SHOULD return list of 2 articles WHEN db containes 2 articles",   async done => {  const articles = [
new Article("java with JVM", 3),
new Article("extracting iron from Mars", 8)
];
await db.createArticle(articles[0]);
await db.createArticle(articles[1]);
request(app)
.get("/articles")
.end((err, res) => {
expect(res.body).toEqual(articles);
done();
});
});

We presumed that we have a Global Object db that interact with the database in some way and we see the Article object appears which represent our model (Article object has a title and pages number).
When you run the test npm test test/acceptance_tests the first one will pass but the second one will fail with an error message Cannot find name 'db'and Cannot find name ‘Article' and that’s ok we know why, because we don’t define Article and db in our project .

Create src/models/article.ts and add the following code to it :

export class Article{  title: string;
pages: number;

constructor(title:string, pages:number){
this.pages = pages;
this.title = title;
}
}

Important Note:
To be honest with you I don’t like the idea of getting or creating the objects directly, I prefer using IOC container like inversify or building my own Service Locator it’s much cleaner, but I will keep the things simple here.

Round 3:

Let’s put visual image to explain what we know from our tests about our project architecture :

All we know is that we have an db object who interact with the database in some way and an app object who has request handler.

You know what ! our app object has a lot of responsibilities ( Web server, routing, request handlers) and that what makes testing very hard so let’s separate those responsibilities.

For routing let create routes.ts inside src folder:

import * as express from "express";
import ArticleController from './controllers/article_controller';
const router = express.Router();router.get("/articles", ArticleController.index);export { router };

For request handler we create controllers/article_controller.ts inside src folder:

import { Response, Request } from "express";class ArticleController {  index = async (req: Request, res: Response) => {
}
}export default new ArticleController();

and update server.ts:

import * as express from "express";
import * as morgan from "morgan";
import { router } from "./routes";
const app = express();app.use(morgan("dev"));
app.use(router);
app.listen(3000, () => console.log("[SERVER] is up and running on 3000 ..."));export{app}

Round 4:

To unit test our ArticleController we need to mock our http request using moxios and mock also ArticleManager (to discover its APIs) which I don’t want to get into that, you can do it if you want, I want to get to Prisma ASAP.

When we hit /articles the ArticleController.index will receive the request and will ask ArticleManager to get those articles.

adding ArticleManager to the equation

ArticleController.ts :

import { Response, Request } from "express";
import { ArticleManager } from "../managers/article_manager";
class ArticleController { private articleManager: ArticleManager; constructor(){
this.articleManager = new ArticleManager();
}
index = async (req: Request, res: Response) => { const articles = await this.articleManager.getArticles();
res.json(articles);
}}export default new ArticleController();

Round 5: important round

Now we have to unit test our ArticleManager to know much about it’s dependencies.

from the previous Round we knew that ArticleManager has getArticles() method.
Create src/managers/article_manager.ts and add this code to it:

import { Article } from "../models/article";export class ArticleManager {  async getArticles(): Promise<Article[]> {

}
}

Create test/unit_tests/article_manager.test.ts and the following code to it:

import { ArticleManager } from "../../src/managers/article_manager";
import { Article } from "../../src/models/article";
test("getArticles_noArticlesInDB_emptyList", async () => { const articleManager = new ArticleManager(); const articles: Article[] = await articleManager.getArticles(); expect(articles.length).toBe(0);});

Run the test → fails → update the ArticleManager class:

import { Article } from "../models/article";export class ArticleManager {  async getArticles(): Promise<Article[]> {    return [];
}
}

Run the test → pass → refactor → nothing to refactor → add new test

test("getArticles_oneArticleInDB_ListOfOneArticle", async () => {  const articleManager = new ArticleManager();  const articles: Article[] = await articleManager.getArticles();  expect(articles.length).toBe(1);});

Run the test → fails → update the ArticleManager class:
We need to add new dependency to fix the test, ArticleManager should interact with Repository to get those articles.

adding Repository to the equation

Create src/contracts/repository.ts and the following code to it:

import { Article } from "../models/article";export interface Repository  {  articles(): Promise<Article[]>;}

article_manager.ts:

import { Article } from "../models/article";
import { Repository } from "../contracts/repository";
export class ArticleManager {
private repo: Repository;
constructor(repo: Repository){
this.repo = repo;
}

async getArticles(): Promise<Article[]> {
const articles = await this.repo.articles();
return articles;
}}

Let’s fix our test test/unit_tests/article_manager.test.ts:

import { ArticleManager } from "../../src/managers/article_manager";
import { Article } from "../../src/models/article";
import { Repository } from "../../src/contracts/repository";
test("getArticles_noArticlesInDB_emptyList", async () => { const articleManager = new ArticleManager(new EmptyMockRepository()); const articles: Article[] = await articleManager.getArticles(); expect(articles.length).toBe(0);});test("getArticles_oneArticleInDB_ListOfOneArticle", async () => { const articleManager = new ArticleManager(new SingleMockRepository()); const articles: Article[] = await articleManager.getArticles(); expect(articles.length).toBe(1);});class EmptyMockRepository implements Repository {
async articles(): Promise<Article[]> {
return [];
}
}
class SingleMockRepository implements Repository {
async articles(): Promise<Article[]> {
return [new Article("Lord Of The Rings", 5)];
}
}

As you see I mock the Repository wih SingleMockRepository and EmptyMockRepository.
Rerun the test → pass → refactor → next.

Round 6: Prisma has come

Now we know what APIs the Repository has to offer for ArticleManager. We need a concrete implementation for that, So let’s start with an Integration test.
Create src/integration_tests/article_manager.test.ts and the following code to it:

import { ArticleManager } from "../../src/managers/article_manager";
import { Article } from "../../src/models/article";
test("getArticles_noArticlesInDB_emptyList", async () => {
const articleManager = new ArticleManager(new PrismaRepository());

const articles:Article[] = await articleManager.getArticles();
expect(articles.length).toBe(0);
});
test("getArticles_oneArticleInDB_ListOfOneArticle", async () => {
const articleManager = new ArticleManager(new PrismaRepository());

const articles: Article[] = await articleManager.getArticles();

expect(articles.length).toBe(1);
});

Run the test → fails → let’s fix that.

Create src/resources/prisma_repository.ts and add the following code to it:

import { Repository } from "../contracts/repository";
import { Article } from "../models/article";
export class PrismaRepository implements Repository{ async articles(): Promise<Article[]> {

}
}

Add the following to test/integration_tests/article_manager.test.ts:
import {PrismaRepository} from “../../src/resources/prisma_repository";

Prisma:

So, What is Prisma ?!
Prisma is a data layer that replaces traditional ORMs in your application architecture. WHAT?? HOW??
I’m sure you know mongoose the library you use to interact with MongoDB , TypeORM the library you use to interact with SQL databases like Mysql, SQLite … etc, and many other libraries. Now you don’t need any one of them to build high performant, scalable DAL. Prisma solves all of this problems and more, it provides you with a nice GraphQL API Abstraction to interact with your database.

Let me explain to you in simple way how Prisma works.
Prisma basically consists of three main components:

  • Prisma Server : you use it to host Prisma services.
  • Prisma Service : provides GrpahQL CRUD mapping for your database.
  • Prisma Client : it makes the API Server able to interact with Prisma Server (connects to the Prisma Service) and it’s auto generated, we will see that in just a minute.

Enough talking, let’s continue with our example and see what Prisma can do to us.

Installing Prisma:

npm i -g prisma 

At the root of your project create prisma folder and initialize it with commands:

mkdir prisma
cd prisma
prisma init // this will ask you couple of questions, choose Create
new data base , then choose MongoDB , then choose
Prisma Typescript Client. Couple of files and one
folder will be generated.

I have Mongodb installed locally on my PC. So the configuration will be for local database.
Now, to use Prisma server locally it has to be run with docker.
If you don’t know docker that’s not a big problem here, But I recommend to have a look at this tutorial “https://medium.com/@pavsidhu/docker-an-explanation-for-beginners-6356b5202460”.

docker-compose.yml is compose file has the services you want to install which is Prisma and if you want, you can run MongoDB instance with docker so you can add mongo service next to prisma service.

docker-compose.yml

connector field is what Prisma server use to connect to the database, it’s a bridge between the Prisma server and the database (here the database is MongoDB).

database is the database name inside MongoDB.

Now, inside prisma folder run the command docker-compose up -d this will install the images and start the Prisma container. Now we have Prisma Server up and running.

How to deploy Prisma service on Prisma server ?

Each Prisma service has two configuration files :

  • prisma.yml: this is the root config file for Prisma Service :
endpoint: http://localhost:4466    # where the service will
be available
datamodel: datamodel.prisma
databaseType: document # because we use mongodb
generate:
- generator: typescript-client
output: ./generated/prisma-client/
  • datamodel.prisma: it defines the database schema, the models inside it are written in GraphQL SDL , take a look at here to learn about it.
type Article {
id: ID! @id
title: String!
pages: Int!
}

Now let’s run the command prisma deploy , a lot of magic will happen.

  • Prisma connects to MongoDb.
  • prisma_api_db is created and database tables are generated based on the datamodel.
  • Prisma database schema (GraphQL Schema) is generated based on the datamodel.

NOTE:
you have to download two packages npm i -D @types/graphql prisma-client-lib . prisma-client-lib includes graphql package besides many packages needed by Prisma Client

Now run the command prisma generate this will create:
prisma/generated/prisma-client/index.ts
prisma/generated/prisma-client/prisma-schema.ts

prisma-schema.ts will have the Prisma GraphQL Schema generated by Prisma service based on the datamodel.

index.ts will know how to connect to your Prisma Service and gives you a lot of operations which mirrors the GraphQL operations of our Prisma Service.

Back to our code, let’s modify prisma_repository.ts :

import { Repository } from "../contracts/repository";
import { Article } from "../models/article";
import { prisma } from "../../prisma/generated/prisma-client/index";
export class PrismaRepository implements Repository{ async articles(): Promise<Article[]> {
return await prisma.articles().$fragment("{ title pages}");
}
}

$fragment is based on GraphQL, it lets you exclude the fields you don’t want.
See how Prisma is beautiful 💚.

I notice that I forget to clean the database before each test let’s fix that usinbg Prisma :

import { ArticleManager } from "../../src/managers/article_manager";
import { Article } from "../../src/models/article";
import { prisma as db } from "../../prisma/generated/prisma-client";
test("getArticles_noArticlesInDB_emptyList", async () => { await db.deleteManyArticles({});
const articleManager = new ArticleManager(new PrismaRepository());

const articles:Article[] = await articleManager.getArticles();
expect(articles.length).toBe(0);
});
test("getArticles_oneArticleInDB_ListOfOneArticle", async () => { await db.deleteManyArticles({});
const articleManager = new ArticleManager(new PrismaRepository());

const articles: Article[] = await articleManager.getArticles();

expect(articles.length).toBe(1);
});

Now let’s run our integration tests npm test test/integration_tests
Yeaah!! the green is shining 🍏.

Now back to our acceptance tests, let’s modify them a little:

import * as request from "supertest";
import { app } from "../../src/server";
import { Article } from "../../src/models/article";
import { prisma as db } from "../../prisma/generated/prisma-client";
describe("GET /articles", () => { it("SHOULD return 200Ok", done => { await db.deleteManyArticles({});
request(app)
.get("/articles")
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
}); it("SHOULD return list of 2 articles WHEN db containes 2 articles", async done => { const articles = [
new Article("java with JVM", 3),
new Article("extracting iron from Mars", 8)
];
await db.deleteManyArticles({});
await db.createArticle(articles[0]);
await db.createArticle(articles[1]);
request(app)
.get("/articles")
.end((err, res) => {
expect(res.body).toEqual(articles);
done();
});
});
});

Now run our acceptance tests npm test test/acceptance_tests and PASS 💎.

Our final Architecture

I hope you have learned something from this article. I skipped a lot of things to keep the article as simple as possible. This article should have been a series of articles but because it is my first article I kept it single. Next ones will be better.

--

--