Tutorial 1: Prisma PostgreSQL Node.js typescript express and serverless with deployment to Vercel

Blog Image

Tutorial 1: Prisma PostgreSQL Node.js Typescript Express and serverless with deployment to Vercel

To explore serverless with Node.js with deployment to Vercel, here are the good steps to follow:


Technical Tools:

  • VSCode

  • Google Chrome or other web app to access Vercel Account

  • VSCode HTTP Client for convenience (I recommend Thunder Client from extensions on VSCode or you can use any available)

  • (Optionally) Postman if already installed

  • Github account and Git installed on machine

  • Typescript installed on machine, update to latest stable typescript for new features and improvements

  • Yarn installed on machine, or alternatively use npm, pnpm, bun, deno. (This was based on Yarn).


Now, let's begin coding:


Steps:

First Step, create a Node.js with Typescript included.

I am using Windows for this tutorial.

From your choice of folder on your machine, run this:

npm --init -y

Include Typescript support for the app, we'll later add our own customization:

tsc --init

From there, open the code using the code:

code .

Once VSCode is open, let's head to folder, scripting and files setups and customization.

This is the final structure of the app once all Steps will be completed.

Second Step, let's configure our app.

Head to the tsconfig.json and change the code to the following:

{

    "compilerOptions": {

        "allowSyntheticDefaultImports": true,

        "target": "es2016",

        "module": "commonjs",

        "strict": true,

        "esModuleInterop": true,

        "skipLibCheck": true,

        "forceConsistentCasingInFileNames": true,

        "moduleResolution": "node",

        "outDir": "dist",

        "noUnusedLocals": false,

        "noUnusedParameters": false,

        "noImplicitAny": false

    },

    "include": [

        "src/api",

        "prisma/**/*",

        "public/**/*",

        "src/**/*",

        "index.d.ts",

        "src/api"

    ],

    "exclude": ["node_modules"]

}

If there are errors, ignore them for now. Automatically everything will be fixed on the adding files.

Next, Add dependencies packages to our app:

yarn add @prisma/client bcrypt body-parser compression cors dotenv express helmet mz nodemon uuid ws jsonwebtoken

NOTE: not all packages are included here.

Add development dependencies to our app:

NOTE: not all @types/ are installed, manually creating a index.d.ts will help with declaring modules. We'll later explore.

yarn -D add @types/express @types/node @types/uuid prisma ts-node typescript

Once completed, add the following to package.json file under the script section:

"scripts": {
"seed-dev": "ts-node src/seed.ts",
"seed-prod": "node dist/src/seed.js",
"dev": "nodemon src/index.ts",
"compile": "tsc src/index.ts -w",
"build": "yarn clean && tsc && yarn copy-files",
"clean": "rm -rf ./dist",
"copy-files": "cp -r ./prisma/ ./public/ ./dist/",
"start": "node dist/src/index.js",
"migrate": "npx prisma migrate dev && yarn generate",
"generate": "prisma generate",
"push": "npx prisma db push",
"test": "echo \"Error: no test specified\" && exit 1",
"vercel-build": "yarn generate",
"ts.check": "tsc --project tsconfig.json",
"add-build": "git add dist"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
},

It will look like this:

Once complete, The Fun🎉 staff begins:


Create 2 folders at the root of the project like this:

NOTE: Don't worry about the prisma folder, this will be created when initializing prisma client.

Inside the src folder, add the following folders and file:

api

config

controllers

routes

index.ts

These are the basis folders for this app.


Add the following files according to the tree mapping.

  • api

    • index.ts

  • config

    • prisma.config.ts

  • controllers

    • category.controller.ts

  • routes

    • index.ts


Run the following to get the prisma folder:

npx prisma --init

The

prisma

folder will be created with the following in it, except the

seed.ts

and

migrations, data

folder:

Prisma will add

.env

file along its files, which you will add to the .gitignore.

While on .gitignore, create the .gitignore if not existing on the project root.

Add this code to it and personalize for yourself according to the use of the app:

.env
.node-version
/node_modules
/src/public/uploads/*[.jpg, .jpeg, .png, .gif]
/output
/dist
src/*.json
.vercel

Done with Git for now.

Set up database for prisma. I was using PostgreSQL at the moment of this project. Add the postgresql connection strings to the .env file. Alternatively, you can go this link: Databases supported by Prisma to find compatible database and configurations.

Prisma has nice documentation, you can always check it out if you get stuck with their ORM.

Create a schema underneath the datasource object:

Something like this for simplicity:


model Category {
id String @id @unique()
name String @db.VarChar(50)
badge String @db.VarChar(100)
color String @db.VarChar(100)

}


Save the file and move to the next step:

Run the following from your command prompt or terminal:

yarn migrate

NOTE

: Make sure to have internet connection, prisma will require internet connection initially to get some packages for its client.

This will run the migration and create the migrations files in the prisma folder as seen atop.

Next, go the config folder and add this code to the prisma.config.ts

import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();


Done with Prisma setup. Next is are controllers and routes.


Controllers

Everyone is opiniated to using object or class structure for controllers.

I personally just used class structure for this app, I have other projects where I use objects according to ES6+.

Create a file called category.controller.ts

I use a naming scheme or practice by adding a folder name to the end of the file before the extension to counter confusion when a project grows large. This organizes the project structure and readability.

Inside the category.controller.ts add the following code:

import Joi from "joi";
import { prisma } from "../../config/prisma.config";
import { Request, Response } from "express";

class CategoryController {
async categories(req: Request, res: Response) {
try {
const data = await prisma.category.findMany({});
res.status(200).json(data);
} catch (error: any) {
res.status(500).json({ message: error.message });
throw error;
}
}

async category(req: Request, res: Response) {
try {
const id = req.params.id;
const data = await prisma.category.findUnique({
where: {
id: id,
},
});
res.status(200).json(data);
} catch (error: any) {
res.status(500).json({ message: error.message });
throw error;
}
}

public async add(req: Request, res: Response) {
try {
const body = req.body;
const schema = Joi.object({
id: Joi.string().min(1).required(),
name: Joi.string().min(1).required(),
badge: Joi.string().min(1).required(),
color: Joi.string().min(1).required(),
});
const validateRes = schema.validate(body);
if (validateRes.error) {
return res.status(400).send(validateRes.error.details[0].message);
}
const category = await prisma.category.create({ data: body });
res.status(201).json(category);
} catch (error: any) {
res.status(500).json({ message: error.message });
throw error;
}
}

public async update(req: Request, res: Response) {
try {
const id = req.params.id;
const body = req.body;
const schema = Joi.object({
id: Joi.string().min(1).required(),
name: Joi.string().min(1).required(),
badge: Joi.string().min(1).required(),
color: Joi.string().min(1).required(),
});
const validateRes = schema.validate(body);
if (validateRes.error) {
return res.status(400).send(validateRes.error.details[0].message);
}
const category = await prisma.category.update({
where: {
id: id,
},
data: body,
});
res.status(201).json(category);
} catch (error: any) {
res.status(500).json({ message: error.message });
throw error;
}
}

public async remove(req: Request, res: Response) {
try {
const id = req.params.id;
const data = await prisma.category.findUnique({ where: { id: id } });
if (!data) {
return res.status(400).send("Category does not exist");
}
await prisma.category.delete({ where: { id: id } });
res.status(204).json({ result: `Successfully removed ${id}` });
} catch (error: any) {
res.status(500).json({ message: error.message });
}
}
}

export const categoryController = new CategoryController();

Although I did not use Joi for validation at this point of the tutorial, feel free to explore its use for your own projects.

Using object would be like:

export const categoryController ={
categories : async ()=>{}
}

The controller is completed, moving to the routes


Routes

Go to the routes folder, add the following code to the index.ts

import { categoryController } from "../../controllers/category.prisma.controller";

const router = require("express")(); // import express from "express";

//Initialize express when using import

// const router = express();

router.get("/categories", categoryController.categories);

router.get("/category/:id", categoryController.category);

Add other routes... like POST, PUT, DELETE.

NOTE: Make sure the function or method matches the route you are calling.


Next, is to add code to the index.ts in api folder.

import cors from "cors";
import dotenv from "dotenv";
import bodyParser from "body-parser";
import compression from "compression";
import helmet from "helmet";
import express from "express";
import router from "../routes";

dotenv.config();

const app = express();
/*
Configurations
*/
app.use(
cors({
origin: "*",
})
);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(compression());
app.use(helmet.crossOriginResourcePolicy({ policy: "cross-origin" }));
app.use("/api", router);
/*
Routes
*/
app.all("*", (req, res) => {
res.status(400).json({ error: `Route ${req.originalUrl} not found` });
});

export default app;


That is it for the api code.

Remember the index.ts inside the src folder. It is alone in there.

Add this code to it:

import express from "express";
import cors from "cors";
import compression from "compression";
import helmet from "helmet";
import bodyParser from "body-parser";
import dotenv from "dotenv";
import { router } from "./routes/index";

const app = express();

const port = process.env.PORT || 5000;
dotenv.config();


app.use(
cors({
origin: "*",
})
);

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(compression());
app.use(helmet.crossOriginResourcePolicy({ policy: "cross-origin" }));
app.use("/api", router);
app.all("*", (req, res) => {
res.status(400).json({ error: `Route ${req.originalUrl} not found` });
});

app.listen(port, function () {
console.log(`Server Started: http://localhost:${port}/api/`);
});



This above will be used for local testing.

You are actually done with the most part of the app. Next steps will be to add some code to vercel.json and add to git to this project.

Add this code to the vercel.json

{
"version": 2,
"builds": [
{
"src": "src/api/index.ts",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "src/api/index.ts"
}
]
}


This code is well explained by Vercel in their docs here

I leave the local testing to you. Next Blog will be about adding this project to GitHub and deploying to Vercel.

Keep a look out for the next blog.