Building scalable Node.js, Express with TypeScript, Deploy DynamoDB Locally, No AWS account required

Cover Image for 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

  1. Create a new file called app.ts in the root of your project directory.
  2. 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

  1. 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"]

  1. Build the Docker image: docker build -t my-app .
  2. 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.


More Stories