Building scalable Node.js, Express with TypeScript, Deploy DynamoDB Locally, No AWS account required
Building Express Node.js REST API with TypeScript to Perform CRUD Operations on DynamoDB and Deploying on Docker: A Comprehensive Guide
Pre-requirement
- Basic knowledge of TypeScript, Node.js, Express.js, and Docker
- Familiarity with AWS DynamoDB service
- Docker installed on your local machine
Step 1: Create a new Express.js project and Set up TypeScript
- Create a new file called
app.ts
in the root of your project directory. - Add the following code to
app.ts
to set up the basic Express.js server:
//app.ts
import dotenv from "dotenv";
import express, { Request, Response, } from "express";
import programsRouter from "./routes/programs";
dotenv.config();
const app = express();
const port = 8080 || process.env.PORT;
app.use(express.json());
app.use("/programs", programsRouter);
if (process.env.NODE_ENV !== "test") {
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
}
export default app;
1. Createroutes
folder and Programs.ts
to setup REST API route
//routes/programs.ts
import express from "express";
import getAllPrograms from "../controllers/getAllPrograms";
import addProgram from "../controllers/addProgram";
import deleteProgram from "../controllers/deleteProgram";
import updateProgram from "../controllers/updateProgram";
const router = express.Router();
// Route to get all programs
router.get("/", getAllPrograms);
// Route to add a program
router.post("/", addProgram);
// Route to delete a program
router.delete("/", deleteProgram);
// Route to update a program
router.put("/", updateProgram);
export default router;
Step 2: Set up DynamoDB
1. Create dbClient.ts
to connect DynamoDB locally
// database/dbClient.ts
import dotenv from "dotenv";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
dotenv.config();
const awsConfig = {
accessKeyId: "DUMMYIDEXAMPLE" || process.env.AWS_ACCESS_KEY_ID as string,
secretAccessKey: "DUMMYEXAMPLEKEY" || process.env.AWS_SECRET_ACCESS_KEY as string,
};
const dbClient = new DynamoDBClient({
region: "localhost" || process.env.AWS_REGION as string,
endpoint: "http://localhost:8000" || process.env.DYNAMODB_ENDPOINT_URL as string,
credentials: awsConfig,
});
export default dbClient;
2. Create table
// database/createTable.ts
import {
CreateTableCommand,
CreateTableCommandInput,
ListTablesCommand,
} from "@aws-sdk/client-dynamodb";
import dbClient from "./dbClient";
async function checkIfTableExists(tableName: string): Promise<boolean> {
const result = await dbClient.send(new ListTablesCommand({}));
return result.TableNames?.includes(tableName) ?? false;
}
export async function createTable(
tableName: string,
tableParams: CreateTableCommandInput
) {
try {
const tableExists = await checkIfTableExists(tableName);
if (!tableExists) {
await dbClient.send(new CreateTableCommand(tableParams));
console.log("Successfully created table");
} else {
console.log("Table already exists. Skipping creation.");
}
} catch (err) {
console.error(err);
throw new Error(`Failed to create table: ${err}`);
}
}
3. Write some example data to DynamoDB
// database/writeData.ts
import {
BatchWriteCommand,
BatchWriteCommandInput,
} from "@aws-sdk/lib-dynamodb";
import * as fs from "fs";
import * as R from "ramda";
import path from "path";
import dbClient from "./dbClient";
import type { Programs } from "../../types/program";
async function writeData(tableName: string, items: Programs) {
const dataSegments = R.splitEvery(25, items);
try {
// Loop batch write operation
for (let i = 0; i < dataSegments.length; i++) {
const segment = dataSegments[i];
const putRequests = segment.map((item) => {
return {
PutRequest: {
Item: {
id: item.id,
title: item.title,
topic: item.topic,
learningFormats: item.learningFormats,
bestseller: item.bestseller,
startDate: item.startDate,
},
},
};
});
const params: BatchWriteCommandInput = {
RequestItems: {
[tableName]: putRequests,
},
};
dbClient.send(new BatchWriteCommand(params));
}
console.log("Success writing data to DynamoDB");
} catch (error) {
console.log("Error", error);
}
}
export const writePrograms = async function (tableName: string) {
const absolutePath = path.resolve(
__dirname,
"../../data/example-programs.json"
);
const allPrograms = JSON.parse(fs.readFileSync(absolutePath, "utf8"));
await writeData(tableName, allPrograms);
};
4. Initialise DB by giving TableName
and Primary Key
// database/initialize.ts
import { createTable } from "./createTable";
import { writePrograms } from "./writeData";
const TableName = "Programs";
const createTableParams = {
TableName: TableName,
KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
AttributeDefinitions: [{ AttributeName: "id", AttributeType: "N" }],
BillingMode: "PAY_PER_REQUEST",
};
async function initialize() {
await createTable(TableName, createTableParams);
await writePrograms(TableName);
}
initialize();
Step 3: Implement CRUD operations
- First try get all data from table "Programs"
// controller/getAllPrograms.ts
import { Request, Response } from "express";
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
import dbClient from "../database/dbClient";
const TableName = "Programs";
const docClient = DynamoDBDocument.from(dbClient);
const getAllPrograms = async (req: Request, res: Response) => {
try {
const params = {
TableName: TableName,
};
const result = await docClient.scan(params);
// Convert the learningFormats Set object to an array
if (result.Items) {
const items = result.Items.map((item) => ({
...item,
learningFormats: Array.from(item.learningFormats),
}));
return res.send(items);
}
} catch (error) {
console.log(error);
return res.status(500).send("Error retrieving programs");
}
};
export default getAllPrograms;
2. Add programs
// controllers/addProgram.ts
import { Request, Response } from "express";
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
import dbClient from "../database/dbClient";
import type { Program } from "../../types/program";
const TableName = "Programs";
const docClient = DynamoDBDocument.from(dbClient);
const addProgram = async (req: Request, res: Response) => {
try {
const program: Program = req.body;
const { id, title } = program;
const checkParams = {
TableName: TableName,
Key: {
id: id,
},
};
// Check if a program with the same ID already exists
const { Item: existingPrograms } = await docClient.get(checkParams);
if (existingPrograms) {
return res.send(`Program with id ${id} already exists`);
} else {
const params = {
TableName: TableName,
Item: program,
};
// Add the program to the database
await docClient.put(params);
return res.send(`Program with id ${id} title ${title} has been added!`);
}
} catch (error) {
console.log(error);
return res.status(500).send("Error adding program");
}
};
export default addProgram;
3. Update programs
// controller/updateProgram.ts
import { Request, Response } from "express";
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
import dbClient from "../database/dbClient";
import type { Program } from "../../types/program";
const TableName = "Programs";
const docClient = DynamoDBDocument.from(dbClient);
const updateProgram = async (req: Request, res: Response) => {
const program: Program = req.body;
const { id } = program;
try {
const params = {
TableName: TableName,
Key: { id: id },
UpdateExpression:
"SET title = :title, topic = :topic, learningFormats = :learningFormats, bestseller = :bestseller, startDate = :startDate",
ExpressionAttributeValues: {
":title": program.title,
":topic": program.topic,
":learningFormats": program.learningFormats,
":bestseller": program.bestseller,
":startDate": program.startDate,
},
ReturnValues: "UPDATED_NEW",
};
// if the program does not exist, return an error
const { Item: existingPrograms } = await docClient.get(params);
if (!existingPrograms) {
return res.send(`Program with id ${id} does not exist`);
} else {
// Update the program
const results = await docClient.update(params);
return res.send(
"Program updated successfully: " +
JSON.stringify({ id, ...results.Attributes })
);
}
} catch (err) {
console.error(err);
return res.status(500).send("Error updating program in DynamoDB");
}
};
export default updateProgram;
4. Delete Program
// controller/deleteProgram.ts
import { Request, Response } from "express";
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
import dbClient from "../database/dbClient";
import type { Program } from "../../types/program";
const TableName = "Programs";
const docClient = DynamoDBDocument.from(dbClient);
const deleteProgram = async (req: Request, res: Response) => {
try {
const program: Program = req.body;
const { id, title } = program;
const params = {
TableName: TableName,
Key: { id: id },
};
// check if the program exists
const { Item: existingPrograms } = await docClient.get(params);
if (!existingPrograms) {
return res.send(`Program with id ${id} does not exist`);
} else {
// Delete the program
await docClient.delete(params);
return res.send(
`Program with id: ${id} title ${title} has been deleted!`
);
}
} catch (error) {
console.log(error);
return res.status(500).send("Error deleting program");
}
};
export default deleteProgram;
Step 4: Build and run the application
- Define
Dockfile
to build app-node image
# Use an official Node.js runtime as a parent image
FROM node:18-alpine
# Set the working directory to /app
WORKDIR /app
# Copy the package.json and package-lock.json files to the container
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code to the container
COPY . .
# Build the application
RUN npm run build
# Expose port for the application to listen on
EXPOSE 8080
# Start the application
CMD ["npm", "start"]
- Define
docker-compose.yml
to run both APP API and DynamoDB locally
// docker-compose.yml
version: "3.8"
services:
dynamodb-local:
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
image: "amazon/dynamodb-local:latest"
container_name: dynamodb-local
ports:
- "8000:8000"
volumes:
- "./docker/dynamodb:/home/dynamodblocal/data"
working_dir: /home/dynamodblocal
app-node:
image: "my-app:latest"
container_name: app-node
ports:
- "8080:8080"
depends_on:
- "dynamodb-local"
links:
- "dynamodb-local"
environment:
AWS_ACCESS_KEY_ID: "DUMMYIDEXAMPLE"
AWS_SECRET_ACCESS_KEY: "DUMMYEXAMPLEKEY"
AWS_REGION: "localhost"
DYNAMODB_ENDPOINT_URL: "http://dynamodb-local:8000"
command:
["sh", "-c", "sleep 3 && npm run seed && npm start"]
- Build the Docker image:
docker build -t my-app .
- Run
docker-compose up
to start the both DynamoDB-Local and the API app
Now, we can access the application at http://localhost:8080
.
Congratulations! You have now built an Express Node.js REST API with TypeScript to perform CRUD operations on DynamoDB.