- Published on
Kidocode Workshop — express.js with SQL database
15 min read|0 views
- Authors
- Name
- N4O
- @nao0809_

Table of Contents
QoL Extension for VSCode
Backend Section
- Prepare
yarn
if you haven’t (yarn is a package manager for nodejs) - Create a new folder with the name
simple-passwd-manager
or anything you want - Open terminal in that folder and type:
yarn init
- The above command will initialize your project, follow it properly
- Now, let’s install some of our development tools and packages (go to #package explanation for explanation of our packages)
- express.js:
yarn add express express-session
(express-session
to manage our login session) - TypeScript:
yarn add --dev typescript ts-node ts-node-dev @types/node @types/express @types/express-session
- Prisma:
yarn add --dev prisma
andyarn add @prisma/client
- express.js:
- Create the following folder:
public
,prisma
, andsrc
TypeScript
We first need to prepare our typescript project, after finishing the above section. Run the following command: npx tsc --init
The above command will create a new tsconfig.json which you need to edit
Enable
resolveJsonModule
sourceMap
esModuleInterop
forceConsistentCasingInFileNames
strict
skipLibCheck
Set
target
->es2016
lib
->["ESNext", "ESNext.Array"]
module
->commonjs
outDir
->./dist
Then open your package.json file, and add scripts
key if it's missing and add the following dictionary data:
[...]
"scripts": {
"start": "node ./dist/index.js",
"dev": "ts-node --transpile-only ./src/index.ts",
"build": "tsc",
"watch": "ts-node-dev --respawn --transpile-only ./src/index.ts",
},
[...]
Prisma ORM (Database)
Now, Let's setup our database system, we will be using Prisma ORM to help map our document into TypeScript or JavaScript.
In the same folder, type in terminal: npx prisma init --datasource-provider sqlite
this command will initate prisma with SQLite backend.
After that, open the prisma/schema.prisma
file and let's create our database schema:
- Our database need user model and our password collection model
- Name our user model:
User
and password collection asPasswordBank
model User {
}
model PasswordBank {
}
Inside User
, we need to define our user id, email, password, and their password collection.
model User {
id Int @id @default(autoincrement())
email String @unique
password String // plaintext password, not hashed
banks PasswordBank[]
}
Explanation!
@id @default(autoincrement())
set ourid
key as the SQL ID, and the default will be an auto increment number@unique
will make our email unique and not allow any duplicates[]
symbol will mark that as a list (soPasswordBank[]
would be a list ofPasswordBank
)
Inside PasswordBank
, we need to define the ID, email, and password. We also need to add relation to our User
data that will mark that specific password data for specific user.
model PasswordBank {
id Int @id @default(autoincrement())
email String
password String
user User @relation(fields: [userId], references: [id])
userId Int
lastUpdated DateTime @default(now())
}
Explanation!
User @relation(fields: [userId], references: [id])
would means that we are referencing ourUser
model for thisPasswordBank
, thefields
area will be one of the fields in thePasswordBank
while thereferences
will be any field in theUser
model.DateTime @default(now())
would set the current time
After that, we need to generate our prisma client data.
Execute:
npx prisma migrate dev --name init
(create a migration data)npx prisma generate
generate the actual data to be used in our script.
Source Code
src/utils.ts
Our utility files include some file that we will generally use multiple times.
We first need to create a isNone
function which will check if a value is null
or undefined
/**
* Check if our value is undefined or null
* @param value Value to be checked
* @returns True if value is null or undefined
*/
export function isNone(value) {
return value === null || value === undefined;
}
To make it TypeScript friendly and we can see all the type hints and warning, we need to mark our function with some TS-only feature.
type Undefined = null | undefined;
export function isNone(value: any): value is Undefined {
return value === null || value === undefined;
}
The above typing basically tells our TS compiler properly.
After that, let's create wrapJSON
function which will make our JSON response consistent.
/**
* Wrap our data response in an unified response format
* @param res Response object from express
* @param data The data we will sent
* @param error The error message we will sent
* @param code The error code
*/
export function wrapJSON(res, data?: any, error?: string, code?: number) {
if (isNone(data)) {
res.json({
error: error || (code === 200 ? 'Success' : 'Unknown Error'),
code: code || 500,
success: code === 200,
});
} else {
res.json({
data,
error: error || (code === 200 ? 'Success' : 'Unknown Error'),
code,
success: code === 200,
});
}
}
The JSDoc explains more about what this code does.
If you want to type res
parameter so TS knows what it is, do this:
import { Response } from 'express';
/**
* Wrap our data response in an unified response format
* @param res Response object from express
* @param data The data we will sent
* @param error The error message we will sent
* @param code The error code
*/
export function wrapJSON(
res: Response<any, Record<string, any>>,
data?: any,
error?: string,
code?: number
) {
if (isNone(data)) {
res.json({
error: error || (code === 200 ? 'Success' : 'Unknown Error'),
code: code || 500,
success: code === 200,
});
} else {
res.json({
data,
error: error || (code === 200 ? 'Success' : 'Unknown Error'),
code,
success: code === 200,
});
}
}
src/types.ts
This file would be our typing stuff that we will reuse sometimes.
You just need to copy paste this code to the file:
export interface IPassword {
id: number;
email: string;
password: string;
lastUpdated: string;
}
src/index.ts
This would be our main entrypoint file for our server project.
We first need to import some of the packages we will need.
import { PrismaClient } from '@prisma/client';
import express from 'express';
import session from 'express-session';
import path from 'path';
import { IPassword } from './types';
import { wrapJSON } from './utils';
The above would import:
PrismaClient
our database connector from Prismaexpress
the express client itselfsession
our session handlerpath
NodeJS pathing packages, useful for ton of stuffIPassword
our defined types fromsrc/types.ts
wrapJSON
our defined function fromsrc/utils.ts
The second thing we need to do is to initalize our express app and our database client. We also want to define our public folder path.
/**
* Initialize our database and express server
*/
const prisma = new PrismaClient();
const app = express();
// Public file directory
const publicPath = path.join(__dirname, '..', 'public');
Explanation!
path.join
will join together a list of parameter that we give together into system friendly path__dirname
is internal variable provided by NodeJS, which tells the current file directory.- The above
publicPath
would basically means go to the upper directory fromsrc
folder thenpublic
directory.
The next thing we need to do is to set our express server to use JSON data and configure our express session handler.
/**
* Use JSON to process our body input
*/
app.use(express.json());
/**
* Configure our express-session
*/
const sessConf: session.SessionOptions = {
// Randomized secret, generated with `openssl rand -hex 32`
secret: 'CHANGETHISTHING',
saveUninitialized: false,
resave: false,
cookie: {},
};
// If the app is running in production mode, enable trust proxy and secure cookies
if (app.get('env') === 'production') {
app.set('trust proxy', 1);
sessConf.cookie!.secure = true;
}
// Bind our session
app.use(session(sessConf));
Explanation and Help!
app.use(express.json());
would use JSON as our body inputsecret: "CHANGETHISTHING",
you need to change this to a proper random secret, you can useopenssl rand -hex 32
or just go to random string generator website- The
app.get("env") === "production"
if statement is to basically use a more secure version for our session handling.
After that, we need to create our API Router
const apiRouter = express.Router();
The above would create a simple router for our API.
Then, let's create our authenticator system (/login
, /signup
, /logout
)
apiRouter.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await prisma.user.findUnique({
where: {
email,
},
});
if (user) {
// Check password
if (user.password === password) {
// @ts-ignore
req.session!.userId = user.id;
req.session.save();
wrapJSON(res, undefined, 'Success', 200);
} else {
wrapJSON(res.status(401), undefined, 'Invalid Password', 401);
}
} else {
wrapJSON(res.status(401), undefined, 'Invalid credentials', 401);
}
});
apiRouter.post('/signup', async (req, res) => {
const { email, password } = req.body;
// Check email if it's been used!
const user = await prisma.user.findUnique({
where: {
email,
},
});
if (user) {
wrapJSON(res.status(409), undefined, 'User already exists', 409);
} else {
const newUser = await prisma.user.create({
data: {
email,
password,
},
});
// @ts-ignore
req.session!.userId = newUser.id;
req.session.save();
wrapJSON(res, undefined, 'Success', 200);
}
});
apiRouter.post('/logout', (req, res) => {
// Destory our session
req.session!.destroy((err) => {
if (err) {
wrapJSON(res.status(500), undefined, 'Error', 500);
} else {
wrapJSON(res, undefined, 'Success', 200);
}
});
});
Explanation!
POST /login
route- We first receive our JSON data from user which should contains email and password
- Then we check if the email exist or not
- If yes, we will then check the password
- If correct, set our session to our
userId
and the return success - If wrong, return an invalid password error
- If correct, set our session to our
- If no, return an invalid credentials error
POST /signup
route- We first receive our JSON data from user which should contains email and password
- Then we check if the email exist or not
- If not, we will then proceed creating our new account
- If yes, we will return User already exists error
POST /logout
route- This route is just a simple session destroyer, after it destory it will just return a success
Refer more to wrapJSON
implementation for more info about how I use the function
After that, let's create a route to access/edit/delete our password bank
apiRouter.get("/passwords", async (req, res) => {
// @ts-ignore
const {userId} = req.session!;
if (!userId) {
wrapJSON(res.status(403), undefined, "Unathorized", 403);
} else {
// Get all passwords related to our user id
const passwords = await prisma.passwordBank.findMany({
where: {
userId,
}
})
// Remap our password results
const remappedPassword: IPassword[] = passwords.map((passwd) => {
return {
id: passwd.id,
email: passwd.email,
password: passwd.password,
lastUpdated: passwd.lastUpdated.toISOString(),
}
})
wrapJSON(res, remappedPassword, undefined, 200);
}
})
apiRouter.post("/passwords", async (req, res) => {
// @ts-ignore
const {userId} = req.session!;
if (!userId) {
wrapJSON(res.status(403), undefined, "Unathorized", 403);
} else {
const {email, password} = req.body;
const newPasswd = await prisma.passwordBank.create({
data: {
email,
password,
userId,
}
})
wrapJSON(res, {
id: newPasswd.id,
email: newPasswd.email,
password: newPasswd.password,
lastUpdated: newPasswd.lastUpdated.toISOString(),
} as IPassword, undefined, 200);
}
})
apiRouter.put("/passwords", async (req, res) => {
// @ts-ignore
const {userId} = req.session!;
if (!userId) {
wrapJSON(res.status(403), undefined, "Unathorized", 403);
} else {
const {email, password, id} = req.body;
const updatedPasswd = await prisma.passwordBank.update({
where: {
id,
},
data: {
email,
password,
lastUpdated: new Date(),
}
})
wrapJSON(res, {
id: updatedPasswd.id,
email: updatedPasswd.email,
password: updatedPasswd.password,
lastUpdated: updatedPasswd.lastUpdated.toISOString(),
} as IPassword, undefined, 200);
}
})
apiRouter.delete("/passwords", async (req, res) => {
// @ts-ignore
const {userId} = req.session!;
if (!userId) {
wrapJSON(res.status(403), undefined, "Unathorized", 403);
} else {
const {id} = req.body;
await prisma.passwordBank.delete({
where: {
id,
},
});
wrapJSON(res, undefined, undefined, 200);
}
})
Explanation!
- In every route, we have our session ID check, making sure if we are properly logged on
- In our
GET
route, we remapped our result to only return relevant data.
After creating all of our API route, we need to bind our router to the main app
// Bind our static folder using express.static so it got served automatically
app.use(express.static(publicPath));
// bind our API router
app.use('/api', apiRouter);
Explanation!
- The first one would basically server our
publicPath
to the internet - The second one is to basically bind our
apiRouter
with the base route of/api
- Everything will turn from
/logout
to/api/logout
- Everything will turn from
After that, let's start our database and server
// start server and before that connect to database
const port = process.env.PORT || 3000;
/**
* Connect to our database, after it got established
* we will then start our server
*/
prisma
.$connect()
.then(() => {
app.listen(port, () => {
console.log(`Starting server on http://localhost:${port}`);
});
})
.catch((err) => {
console.error(err);
process.exit(1);
});
Explanation!
- We first setup a port variable which take from our environment table or fallback to port 3000
- Then we first connect to our database, then using JavaScript promise we use the
.then
function to start our server.
Also, we need to create a custom shutdown handler
// Function to stop our server
function shutdownServer() {
prisma
.$disconnect()
.then(() => {
process.exit(0);
})
.catch((err) => {
console.error(err);
process.exit(1);
});
}
/**
* Bind some exit code that we need to listen
*/
const processCallback = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP'];
processCallback.forEach((signal) => {
process.on(signal, () => {
console.log(`Received ${signal}`);
shutdownServer();
});
});
Explanation!
- The
shutdownServer
works the same way on connecting, but this time we just call an exit - The
processCallback
is a list of callback we can use onprocess
class or function, we basically want to handle all of those signal to call our custom shudtown function
Running our Server
To run our server, we can use:
yarn dev
to start our server in development modeyarn watch
to start our server in development mode and also automatically restart everytime there's changes
We can also build our server and run it using:
yarn build
to build our serveryarn start
to run our server
Frontend Section
Since we already have our project ready, we just need to prepare some more stuff and install more depedencies.
- Install TailwindCSS with
yarn add --dev tailwindcss @tailwindcss/forms
- Prepare our TailwindCSS with
npx tailwindcss init
, this will create atailwind.config.js
file.
Tailwind CSS
We first need to configure our tailwind.config.js
file, just replace yours with this:
const colors = require("tailwindcss/colors");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{css,ts,tsx}",
"./public/**/*.{css,html,js}",
],
theme: {
// change to neutral
extend: {
colors: {
gray: colors.neutral
}
}
},
plugins: [require("@tailwindcss/forms")],
}
Explanation!
- We first import our
colors
fromtailwindcss/colors
to change our default gray color fromslate
toneutral
to have a more gray-ish color - We then extend our
colors
to change ourgray
color toneutral
- We then add our
@tailwindcss/forms
plugin to ourplugins
list
After that we will need to create our tailwind styles file:
- Create
tailwind.css
insrc/styles/
- Add the following code:
@tailwind base;
@tailwind components;
@tailwind utilities;
We also need to add scripts to our package.json
so our tailwind will be compiled:
[...]
"scripts": {
"build:css": "npx tailwindcss -i ./src/styles/tailwind.css -o ./public/static/css/styles.css",
"watch:css": "npx tailwindcss -i ./src/styles/tailwind.css -o ./public/static/css/styles.css --watch"
}
[...]
You can then run build:css
or watch:css
to compile your tailwind styles.
The Webpage View
In this section we will now create our html so we can display and modify our API/Database data.
We will need to create the following file inside the public
folder:
index.html
register.html
dashboard.html
And then, let's open our src/index.ts
file again and atfer the app.use("/api", apiRouter);
line, let's add the following code
// Add custom register route
app.get("/register", (req, res) => {
res.sendFile(path.join(publicPath, "register.html"));
})
// Add our dashboard route
app.get("/dashboard", (req, res) => {
// Check if the user is logged in
// @ts-ignore
if (req.session && req.session.userId) {
res.sendFile(path.join(publicPath, "dashboard.html"));
} else {
res.redirect("/");
}
})
Explanation!
/register
route need to be overriden or it will throw an error (or you need to add.html
to the link)/dashboard
would protect our dashboard route by checking if the user has logged session or not.
Then you can add all the following code to each of your html files.
public/index.html
as our login page
public/register.html
as our registration page
public/dashboard.html
as our dashboard page
You can just copy paste the contents to your own files.
Explanation!
/public/index.html
contents(Login Page)
- This document only includes some simple form data that are does not have any action route since we will override it on our own JS later.
- We only have 2 forms box, which is our email and password input box and also a submit button.
/public/register.html
contents- Mostly the same as our login page, which make our JS content similar too.
/public/dashboard.html
contents- In here, we will use
<table>
to render our password list, we are making our table body empty since we will dynamically add our contents to the pages. - We also have several modal that are hidden until displayed, this modal will be controlled manually using our JS file.
#modalDelete
will be our confirmation modal if we want to delete our data#modalError
will be our modal to show any error occurred to our pages (JS file mainly)#modalAdd
will be our modal to add new password including a custom password generator which the result can be modifiedgenPassLower
is a checkbox to enable lowercase letter (default: on)genPassUpper
is a checkbox to enable uppercase letter (default: off)genPassNum
is a checkbox to enable numbers (default: off)genPassSym
is a checkbox to enable symbols (default: off)genPassLength
is a number input to change our password length (minimum of 6, maximum of 100, and default of 8)btnRegenPass
is a button that allows us to regenerate the results
#modalEdit
instead of adding a new password, this one will be for editing our existing password- A special feature later that we will add is to automatically check the password content and automatically check the checkbox and modify the number value on the go.
- In here, we will use
The JavaScript Magic
Our JavaScript file will be written with ES6 in minds, which will only support modern browser only and not older version of a browser.
We will create our JS file inside the public/static/js
folder, if you haven't created that folder yet, please create it first.
public/static/js/loginApp.js
as our login page JS.
public/static/js/registerApp.js
as our register page JS.
Explanation!
Both code above are mostly the same except some variable and ID change since there's still some difference between login and register page.
document.readyState === "complete"
is a check if our whole page is ready to be modified by DOM, if not we will wait usingDOMContentLoaded
listener.- We also overridden our
submit
event listener for both pagesev.preventDefault();
will prevent the default submit behavior to work, since we want to override it.POSTJson
is a function to basically send our data usingPOST
method to our API.logError
is a way to add error data to our hidden<div>
in our HTMLisEmpty
is an extension of ourisNone
function which will also check if our string is empty, ignoring andy whitespaces
We also have this fucntion wrapper:
(function(){
// code here
})();
The above wrapper will basically execute all of our JS code without revealing it to the window DOM, example are showed in this video below.
The reason is because we do not want outside user to access our function and make it stop working properly by fiddling with it.
public/static/js/dashboardApp.js
as our dashboard page JS.
This JS will be the most complex one and has some function since we might repeat what we do.
Explanation!
generatePassword
is a function that we will use to generate our random password, it accepts some parameters allowing us to modify the results.DashboardState
is a state holder for our program, emulating how React State works.GETJson
andSENDJson
is a wrapper for JSfetch
function and forSENDJson
we can also change the HTTP method we are using (since we have to usePUT
,POST
, andDELETE
)handlePasswordDelete
is a custom function to handle password deletion.- We first fetch our table row using querySelector that use our custom data attribute
- And then we get our deletion button to disable it since we don't want user to spam click it
- After that we send
DELETE
request to our API - We then enable our again button and make our state null again, then we check if we should delete our table row if the deletion success or not
showModal
andhideModal
is self-explanatory, it allows us to hide/show our modal, the function accept single parameter which is the element IDgenerateTableRow
a function to generate our table row data- It basically just try to create a "complex" child of element with the
<tr>
element at the top - In button edit click handler
- We set our password ID to the state
- Then we check our password content for lowercase, capital, symbols, and numbers
- Then we enable checkbox depending on the above result and also adjust the number value according to our password length
- Then we can show our edit modal
- We then set our button edit with custom attribute
- In button delete click handler
- We set the state to our password ID, then show the modal
- We then set our button delete with custom attribute
- We then start appending our element to the top element one by one
- We then return the final table row element with all the relevant child
- It basically just try to create a "complex" child of element with the
isAllGenModifierOff
is a function to check if our password generator modifier checkbox are all unchecked, it's used when we are cycling/regenerating password.disablePasswordModifierCheckbox
andenablePasswordModifierCheckbox
is a function to disable/enable our modifier buttonshouldCloseOrNot
is a function to check if we should close our modal or not (used if user click outside the modal area)displayError
is a simple function to show the error modalhasAnyXXXXXXXXX
is a function to check if letter havex
cycleGeneratedPassword
is a function to regenerate our password and display it to the user- We first get some of our element
- Then we check if all modifier button are unchecked or not, since we don't want to regenerate if all are off
- Then we check all our checkbox state to determine how our generated password will looks like
- Then we can display the result to the user.
Package Explanation
Dependencies
@prisma/client
is our Prisma client, which is used to connect to our databaseexpress
is our web framework, which is used to create our serverexpress-session
is our session middleware, which is used to create our session
Dev Dependencies
@tailwindcss/forms
is our TailwindCSS plugin, which is used to add some form styles@types/express
is our Express types, which is used to add typing hints for our Express packages@types/express-session
is our Express Session types, which is used to add typing hints for our Express Session packages@types/node
is our Node types, which is used to add typing hints for NodeJS stuffprisma
is our Prisma CLI, which is used to manage our Prisma schema and databasetailwindcss
is TailwindCSS, which is used to create our CSS styles dynamically depending on the classes we usets-node
is our TypeScript NodeJS runner, which is used to run our TypeScript codets-node-dev
is our TypeScript NodeJS runner with auto-restart, which is used to run our TypeScript code and automatically restart when there's changestypescript
is our TypeScript compiler, which is used to compile our TypeScript code to JavaScript