TypeScript Objects and Persistence

Building a TypeScript Object Management System with Redis: A Complete Developer’s Lab

Introduction

Masterclass in Modern Backend Architecture: Node.js, TypeScript, and AI-Driven Persistence


The following lab teaches how to build an object oriented TypeScript Application from the "Hello World" basics of Node.js to the sophisticated heights of AI-powered backend architecture. 

The final output is a robust Blog API while mastering the "Modern Stack": Node.js for execution, TypeScript for reliability, Express for routing, and a dual-database strategy using MongoDB for persistent storage and Redis for high-speed, AI-ready memory.

Takeaways:

* Type Safety is Paramount: Utilizing TypeScript and "Branded Types" prevents the common errors that plague large-scale projects by catching bugs during development rather than at runtime.

* Architectural Discipline: Implementing the Model-View-Controller (MVC) and Repository patterns ensures the codebase remains modular, testable, and scalable.

* Security by Design: Authentication is handled via JSON Web Tokens (JWT) and Bcrypt hashing, while specialized middleware acts as a gatekeeper for protected resources.

* AI Integration: The lab leverages cutting-edge tools like Warp Terminal, Cursor AI, and Test Sprite to automate environment setup, code generation, and comprehensive testing.

* Persistence Strategy: Students learn to distinguish between document-based storage (MongoDB) and in-memory data structures (Redis), particularly for AI applications requiring vector search and semantic caching.



1. The Foundation: Why We Build This Way

Imagine you are building a vast library. You could throw all the books in a pile, but you would never find anything. Backend development is the art of building the shelves, the catalog, and the security system for that library. 

We use Node.js because it lets us use JavaScript—the language of the web—on the server. But JavaScript can be a bit loose; it doesn't always tell you when you've made a mistake.

That is why we use TypeScript. Think of TypeScript as a blueprint that insists every piece of wood is measured twice before it's cut. It adds "static typing," which means if you try to put a "User ID" where a "Product ID" should go, the system will stop you before you even turn the machine on.

Core Building Blocks

Tool	Purpose
Node.js	The runtime environment that executes code on the server.

TypeScript	A superset of JavaScript that catches errors early via static typing.

Express.js	A minimalist framework for building the routes (the "doors" of our API).

NPM	The package manager used to install all our third-party "power tools."

Warp Terminal	An AI-powered terminal that translates natural language into system commands.



2. Architectural Patterns: Organizing the Mind

To keep our library from becoming a mess, we use the MVC (Model-View-Controller) pattern. It’s a way of separating concerns so that one part of the code doesn't have to know too much about what the other parts are doing.

* The Model: This is our data logic. It talks to the database (MongoDB). It knows what a "User" or a "Blog" looks like.
* The Controller: This is the brains. It receives the request from the user, decides what needs to happen, and tells the Model what to do. It handles the "flow" but stays away from the raw data logic.
* The View: In an API, the "View" is usually a JSON response—the data we send back to the user's browser or mobile app.

We also use Middleware. You can think of middleware as a series of security guards standing in a hallway. Before a request reaches the Controller, one guard might check if the user is logged in (Authentication), another might check if the data they sent is valid (Validation), and a third might write down what happened in a log (Logger).


3. The Lab Progression Workflow

This lab is structured as a step-by-step journey, moving from a simple server to a fully secured, documented, and tested API.

Phase I: Environment and First Contact

1. AI-Driven Setup: Use Warp Terminal to install Node.js (LTS) and Redis. Instead of memorizing commands, the lab uses natural language prompts like "Install Node.js version 20."

2. Initialization: Initialize the project using npm init -y and install TypeScript and ts-node for direct execution.

3. The Vanilla Server: Create a basic HTTP server using Node’s core modules to understand the raw "Request" and "Response" cycle.

4. Express Hello World: Transition to Express.js to simplify routing. Use nodemon to automatically restart the server whenever code changes.

Phase II: Data Modeling and Persistence

1. MongoDB Connection: Connect to MongoDB Atlas using the Mongoose library. Mongoose provides a "Schema," which is a structured map for our data.

2. The User Model: Define the User schema with fields like name, email (unique), and passwordHash.

3. The Blog Model: Create a schema for blog posts. Importantly, use the "By Reference" approach for images—storing a URL from a service like Cloudinary or a local folder rather than saving the heavy binary data directly in the database.

4. Redis for Speed: Set up a Redis client. For AI-first applications, Redis serves as the "Fast Memory Layer," handling vector searches and conversation state (context) for LLMs.

Phase III: Security and Business Logic

1. Bcrypt Hashing: Never store passwords in plain text. Use the Bcrypt library to "hash" them into a secure string.

2. JWT Authentication: When a user logs in, issue a JSON Web Token (JWT). This token is like a digital ID card that the user sends back with every future request.

3. The "Require Auth" Middleware: Build a function that intercepts requests to protected routes (like creating a blog) and verifies the JWT.

4. Branded Types: Implement a TypeScript pattern called "Branding." This ensures that a UserId string and a BlogId string are treated as different types, preventing you from accidentally querying a user with a blog's ID.

Phase IV: Features and Documentation

1. Image Uploads: Use the Multer library to handle multi-part form data, allowing users to upload images for their blog posts.

2. CRUD Operations: Implement Create, Read, Update, and Delete functionality for the blog posts, ensuring that only the author of a post can edit or delete it.

3. Swagger Documentation: Use Swagger UI to automatically generate a web page that documents every endpoint. This allows other developers to see how the API works and test it without writing any code.



4. Advanced Persistence: Redis for the AI Era

While MongoDB holds our long-term records, Redis is essential for modern AI applications. The lab highlights that Redis is more than a cache; it is a Vector Database.

* Semantic Search: Redis allows us to store "embeddings" (mathematical representations of meaning) from AI models like OpenAI or Claude. This lets us find contextually similar content.

* LLM Context Persistence: AI agents often have "amnesia." Redis acts as a short-term memory, storing conversation history so the AI remembers what the user said three messages ago.

* Performance: Because Redis lives in memory, it delivers microsecond latency—critical when an AI needs to perform multiple database checks during a single interaction.



5. Automated Testing with Test Sprite

The final stage of the lab moves away from manual testing. The lab introduces Test Sprite, an AI-powered platform.

* Automated Test Plans: It interprets your documentation (like a PRD or Swagger spec) to generate test cases.

* Cloud Execution: It runs these tests in a cloud environment and provides a dashboard showing what passed and what failed (e.g., verifying that a user cannot register with a duplicate email).

* Iterative Debugging: If a test fails, the developer can "re-prompt" the AI to rewrite the test or fix the logic until the system achieves a "10 out of 10" pass rate.



Final Review: The Developer's Mindset

Building a backend isn't just about making things work; it's about making things last. 

By using TypeScript for safety, Express for structure, MongoDB and Redis for storage, and AI for speed, you aren't just writing code—you are engineering a system.

As you move through these steps, remember the goal: 

Create a service that is secure, fast, and easy for other developers (and AI) to understand. 

Now, go ahead—open your terminal and start building.

Object-oriented programming combined with persistent storage is fundamental to modern application development.

This hands-on lab demonstrates how to build a TypeScript application that manages objects with Redis as the backend database—all executed directly using ts-node without manual compilation steps.

Whether you’re learning advanced TypeScript patterns, building microservices, or exploring Redis integration, this tutorial provides a complete working example you can run and modify immediately.

Setting Up Your AI-Powered Development Environment with Warp Terminal

Why Warp Terminal Changes Everything for Developers

If you’ve ever spent hours troubleshooting installation commands, wrestling with package managers, or searching Stack Overflow for the exact syntax to configure a database, you know the frustration. Warp AI Terminal eliminates this productivity drain by letting you speak to your system in plain English instead of memorizing arcane command-line syntax.warp+1

Warp is an Agentic Development Environment that combines a traditional terminal with AI superpowers. Instead of being a Linux or macOS systems administrator, you can focus on what you actually want to build—interesting applications. Warp’s AI understands your intent, translates natural language into correct commands, and executes complex multi-step workflows automatically.warp+2[youtube]​

Used by over 700,000 engineers and 56% of Fortune 500 teams, Warp has become the go-to terminal for developers who value speed over syntax memorization. It seamlessly switches between traditional commands and natural language, making it perfect for both experienced developers and those just starting their journey.amplifilabs+1[youtube]​

What Makes Warp Revolutionary

Natural Language System Administration

Warp’s Agent Mode recognizes and interprets plain English directly on the command line. You can type questions or tasks like:[warp]​

  • “Install Node.js version 20”
  • “Start a Redis server on port 6379”
  • “Fix all my import errors”
  • “Delete all my fully merged Git branches”thedataexchange+1

The AI detects natural language locally (nothing leaves your machine until you hit Enter), interprets your intent, generates the correct commands, and can even execute multi-step workflows autonomously.amplifilabs+1

Proactive AI Assistance

Warp doesn’t just wait for you to ask—it actively helps when you encounter problems:[thedataexchange]​

  • Compiler errors: Automatically suggests fixes when builds fail
  • Missing dependencies: Detects version conflicts or missing packages and offers to install them
  • Configuration issues: Identifies common setup problems and provides solutions

This is transformative when setting up development environments. Instead of debugging cryptic error messages, Warp’s AI explains what went wrong and how to fix it.amplifilabs+1

Integration with AI Coding Tools

Warp pairs perfectly with modern AI development tools like Cursor, GitHub Copilot, Cline, and Windsurf. While those tools generate code, Warp handles the command-line operations:[amplifilabs]​

  • Translates AI-generated instructions into terminal commands
  • Debugs runtime and environment errors
  • Validates commands before execution
  • Provides explanations when generated code fails[amplifilabs]​

Installing Warp Terminal

Step 1: Download and Install Warp

Visit warp.dev and download Warp for your operating system (macOS, Linux, or Windows via WSL).warp+1

For macOS:

bashbrew install --cask warp

For Linux:
Download the appropriate package from warp.dev and install following your distribution’s package manager.

Step 2: Launch and Authenticate

Open Warp and sign in with your preferred authentication method. Warp offers a free tier with generous AI credits, perfect for this lab.[youtube]​

Setting Up Node.js, Redis, and VS Code with Warp

Now comes the magic. Instead of searching for installation commands, we’ll use natural language prompts that Warp’s AI will translate into the correct commands for your system.

Installing Node.js

Warp Prompt (just type this into Warp):

textInstall Node.js version 20 LTS and verify the installation

Warp’s AI will detect your operating system and generate the appropriate commands. For example:

  • macOS: brew install node@20
  • Ubuntu/Debian: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs
  • Fedora/RHEL: sudo dnf module install nodejs:20

The AI will then suggest verification commands:

textnode --version
npm --version

Why this works: You don’t need to remember package manager syntax or search for the right repository URLs. Warp understands your intent and handles the platform-specific details.warp+1

Installing Redis

Warp Prompt:

textInstall Redis server and configure it to run on port 6379

Warp will generate platform-specific installation commands. For macOS:

bashbrew install redis
brew services start redis

For Ubuntu/Debian:

bashsudo apt update
sudo apt install redis-server
sudo systemctl start redis-server
sudo systemctl enable redis-server

Verify Redis is running:

textCheck if Redis is running and test the connection

Warp might generate:

bashredis-cli ping

You should see PONG in response, confirming Redis is operational.[redis]​

Bonus Warp Intelligence: If Redis fails to start, Warp’s AI will detect the error and suggest fixes—checking ports, permissions, or configuration issues automatically.[thedataexchange]​

Installing Visual Studio Code

Warp Prompt:

textInstall Visual Studio Code and add it to my PATH

For macOS:

bashbrew install --cask visual-studio-code

For Linux:

bashsudo snap install --classic code

Setting Up Your Project Directory

Warp Prompt:

textCreate a new directory called typescript-redis-lab, initialize it as an npm project, and install TypeScript, ts-node, Redis client, and type definitions

Warp will generate and can execute:

bashmkdir typescript-redis-lab
cd typescript-redis-lab
npm init -y
npm install -D typescript ts-node @types/node
npm install redis
npx tsc --init

Open in VS Code:

textOpen the current directory in Visual Studio Code

Warp generates:

bashcode .

Advanced Warp Workflows for This Lab

Setting Up the Project Structure

Warp Prompt:

textCreate a src directory with files types.ts, models.ts, redis-client.ts, repository.ts, and index.ts

Warp generates:

bashmkdir -p src
touch src/types.ts src/models.ts src/redis-client.ts src/repository.ts src/index.ts

Checking Redis Connection Before Running Code

Warp Prompt:

textShow me if Redis is running and what port it's listening on

Warp might use:

bashredis-cli info server | grep tcp_port
ps aux | grep redis-server

Running Your TypeScript Application

Warp Prompt:

textRun the TypeScript file at src/index.ts using ts-node

Warp generates:

bashnpx ts-node src/index.ts

Debugging Connection Issues

If you encounter connection errors, try:

Warp Prompt:

textRedis connection is being refused. Show me the Redis logs and check if the service is running

Warp will generate platform-specific commands to check service status and logs, troubleshoot firewall issues, or restart Redis if needed.[thedataexchange]​

Troubleshooting with Warp’s AI Intelligence

Dependency Conflicts

Warp Prompt:

textI'm getting a TypeScript version conflict error. Show me what versions are installed and fix any mismatches

Missing Type Definitions

Warp Prompt:

textInstall all missing TypeScript type definitions for my current project

Redis Not Starting

Warp Prompt:

textRedis won't start. Check if port 6379 is already in use and suggest solutions

Warp will check for port conflicts, suggest killing blocking processes, or recommend alternative ports.warp+1

The Warp Development Workflow

Here’s how your development cycle transforms with Warp:

  1. Setup environments in seconds using natural language instead of documentation diving
  2. Run commands confidently because Warp explains what each command does before executing
  3. Debug faster with AI-powered error interpretation and suggested fixes
  4. Share knowledge using Warp Drive to save common commands for your team[thedataexchange]​
  5. Stay in flow without context-switching to Stack Overflow or documentation

Warp vs Traditional Terminal: A Real Example

Traditional Approach:

bash# Google: "how to install redis on macos"
# Read documentation
# Copy brew command
brew install redis
# Google: "how to start redis service"
# Read more documentation
brew services start redis
# Google: "how to verify redis is running"
redis-cli ping
# Error occurs
# Google: "redis connection refused macos"
# Read 10 Stack Overflow threads...

Warp Approach:

textInstall Redis and start it as a service, then verify it's working

Warp generates, explains, and executes all necessary commands. If errors occur, it suggests fixes automatically.warp+2

Pro Tips for Warp

Enable AI Completions

Warp provides inline AI suggestions similar to GitHub Copilot for the command line. As you type, it predicts what you’re trying to accomplish and offers completions.[thedataexchange]​

Use Voice Input

Warp supports voice commands in chat mode. Press the voice button and speak your request instead of typing.[thedataexchange]​

Save Common Workflows

Create reusable workflows for repetitive tasks:

Warp Prompt:

textSave a workflow called "setup-ts-project" that creates a TypeScript project with all my usual dependencies

Collaborate with Warp Drive

Share commands and workflows with your team using Warp Drive, where the AI can semantically search your team’s shared knowledge.[thedataexchange]​

Why This Matters for AI-First Development

As developers, our value isn’t in memorizing Linux commands or package manager syntax—it’s in building intelligent applications that solve real problems. Warp eliminates the “tax” of system administration knowledge, letting you focus on TypeScript, Redis integration, AI features, and application logic.[amplifilabs]​

When you’re integrating LLMs, managing conversation state in Redis, and building AI-powered features, the last thing you want is to debug npm dependency hell or fight with Redis configuration files. Warp handles that operational overhead, keeping you in the creative zone where you build value.warp+1

Your Environment Is Ready

With Warp, Node.js, Redis, and VS Code installed, you’re ready to build the TypeScript-Redis application from our lab. You didn’t need to become a Linux admin, memorize package manager commands, or lose hours to environment configuration.

This is the future of development: natural language infrastructure management powered by AI, freeing developers to focus on what they do best—creating amazing applications.

Now let’s build something intelligent with TypeScript and Redis. Open VS Code, and let’s start coding.


Quick Reference: Essential Warp Prompts for This Lab

textInstall Node.js version 20 LTS and verify the installation
Install Redis server and configure it to run on port 6379
Install Visual Studio Code and add it to my PATH
Create a new TypeScript project with Redis client and ts-node
Show me if Redis is running and what port it's listening on
Run the TypeScript file at src/index.ts using ts-node
Redis connection is being refused. Check logs and suggest fixes

Welcome to development without the operational overhead.

Welcome to Warp.

Preamble: Why Redis is Your AI-First Database for Modern Application Development

The Evolution from Firebase to Redis for AI-Powered Applications

For years, Firebase has been the go-to backend for full-stack developers building mobile and web applications. Its real-time synchronization, excellent mobile SDK support, and tight integration with Google services made it an obvious choice for rapid application development. However, as artificial intelligence transforms application architecture—particularly with LLM integrations, vector search, and context-aware systems—Redis has emerged as the superior choice for AI-first development.[ionos]​

What is Redis?

Redis (Remote Dictionary Server) is an open-source, in-memory data structure store that functions as a database, cache, message broker, and streaming engine. Unlike traditional databases that read from disk, Redis keeps data in memory, delivering microsecond response times that are critical for real-time AI applications. It supports rich data structures including strings, hashes, lists, sets, sorted sets, and crucially for AI applications—vectors.slashdot+2

Why Redis Dominates AI Application Development

1. Native Vector Database Capabilities

Redis includes the RediSearch module, which transforms it into a powerful vector database essential for AI applications. When you call OpenAI, Anthropic’s Claude, or any LLM API, you can store the embeddings directly in Redis and perform semantic similarity searches with incredible speed. This enables:risingwave+1

  • Semantic search: Find contextually similar content, not just keyword matches
  • RAG (Retrieval Augmented Generation): Pull relevant context from your database to augment LLM prompts
  • Memory-enabled agents: Build AI agents that remember past interactions across sessionsredis+1

Firebase has no native vector search capabilities, forcing you to bolt on third-party solutions or perform inefficient full-table scans.peerspot+1

2. Context Persistence for LLM Applications

Modern AI applications require sophisticated memory management. Redis excels at providing both:[redis]​

  • Short-term memory (working context): Using Redis checkpoints, you can maintain conversation state across multiple API calls to your LLM, preserving the full context of ongoing interactions[redis]​
  • Long-term memory (persistent knowledge): RedisStore enables cross-session memory, letting your AI assistant remember user preferences, past decisions, and accumulated knowledge even after context windows expirenews.ycombinator+1

With LangGraph’s Redis checkpoint integration, you can build AI agents that don’t suffer from “amnesia” every time the context limit is reached. This is transformative for coding assistants, customer service bots, and any AI application requiring continuity.news.ycombinator+1

3. Performance at AI Scale

AI applications demand extreme performance:peerspot+1

  • Microsecond latency: Redis’s in-memory architecture delivers responses in microseconds, critical when you’re making multiple database calls during a single LLM interaction[peerspot]​
  • High throughput: Handle thousands of vector similarity searches per second while your application processes streaming LLM responses[risingwave]​
  • Real-time responsiveness: While Firebase is fast for simple CRUD operations, Redis’s in-memory design ensures consistently sub-millisecond response times even under AI workload pressurerisingwave+1

When you’re orchestrating complex AI workflows—calling embeddings APIs, performing vector searches, updating agent state, and streaming responses—every millisecond counts. Redis’s architecture is purpose-built for this.

4. Seamless Integration with Modern AI Stacks

Redis integrates natively with the entire AI ecosystem:cookbook.openai+2

  • OpenAI integration: Store embeddings from OpenAI’s API and perform vector searches using Redis’s built-in capabilitiescookbook.openai+1
  • LangGraph checkpointing: Official Redis checkpoint savers for building stateful AI agents[redis]​
  • Model Context Protocol (MCP): Projects like Recall demonstrate how Redis provides persistent memory for Claude and other LLMs across sessions[news.ycombinator]​
  • Python AI libraries: First-class support in LangChain, LlamaIndex, and other popular AI frameworks

Firebase, designed primarily for mobile/web CRUD operations, lacks these AI-specific integrations and requires custom workarounds.ionos+1

5. Local Development and Control

For AI development, local control matters:

  • Run Redis locally: Develop and test your AI features completely offline without cloud dependencies or API costs
  • Data sovereignty: Keep sensitive conversation history and embeddings on your infrastructure
  • No vendor lock-in: Redis is open-source; you can deploy anywhere (local, cloud, hybrid)

Firebase requires cloud connectivity and locks you into Google’s ecosystem. For AI applications handling proprietary data or requiring air-gapped deployments, this is a significant limitation.[ionos]​

The AI Application Architecture: Redis + LLM APIs

Here’s how Redis transforms your AI application stack:

┌─────────────────────────────────────────┐
│ Your Application (TypeScript/Python) │
└────────────┬────────────────────────────┘

┌────────┴────────┐
│ │
▼ ▼
┌─────────┐ ┌──────────┐
│ Redis │ │ LLM APIs │
│ │ │ │
│ Vectors │ │ OpenAI │
│ Context │◄─────┤ Claude │
│ Cache │ │ Gemini │
└─────────┘ └──────────┘

Typical workflow:

  1. User sends a query to your application
  2. Generate embeddings via OpenAI API → store in Redis
  3. Perform vector search in Redis to find relevant context
  4. Retrieve conversation history from Redis checkpoints
  5. Build augmented prompt with context + history
  6. Call LLM API with enriched context
  7. Stream response to user while updating Redis state
  8. Store new conversation turn in Redis for future retrieval

This architecture is nearly impossible to replicate efficiently with Firebase.

When Firebase Still Makes Sense

Firebase remains excellent for:

  • Simple mobile apps with real-time sync requirements
  • Applications deeply integrated with Google services
  • Projects prioritizing ease of setup over AI capabilities
  • Teams without DevOps resources for database management[ionos]​

However, if your application involves LLM integration, semantic search, or AI agents with memory, Redis provides capabilities that Firebase simply cannot match.slashdot+2

Redis for the AI Era

As you integrate AI into your applications, the database becomes more than just persistent storage—it becomes your AI’s memory system, context manager, and semantic search engine. Redis’s in-memory architecture, native vector support, and ecosystem integrations make it the ideal foundation for AI-first development.[redis]​

The lab that follows demonstrates this philosophy in practice: building TypeScript applications where Redis doesn’t just store data, but enables intelligent, context-aware AI features that remember, learn, and respond with unprecedented speed and sophistication.

Welcome to AI-first application development. Welcome to Redis.

Lab Outline & Learning Objectives

1. TypeScript Object Fundamentals

Learn how to create, type, and work with objects using interfaces and classes. This foundation ensures type safety throughout the application and demonstrates why TypeScript is essential for maintainable codebases.[typescriptlang]​

2. Branded Types for Domain Safety

Implement branded types to prevent ID mix-ups and enforce domain rules at compile time. This pattern becomes critical when working with multiple entity types (Users, Products, Orders) to ensure a UserId can never be accidentally used where a ProductId is expected.madewithlove+1

3. Type Casting and Array Operations

Master retrieving objects from arrays and casting them back to their proper types. This skill is essential when working with Redis, where data comes back as generic types that need to be restored to your domain models.w3schools+1

4. Redis Integration for Persistence

Connect TypeScript to Redis using modern client libraries, storing and retrieving complex objects using JSON serialization. Redis provides lightning-fast data access while maintaining persistence across application restarts.github+2

5. ts-node Development Workflow

Use ts-node for rapid development, running TypeScript directly without separate compilation steps. This approach streamlines the development process and keeps focus on object-oriented design rather than build tooling.github+1

Prerequisites

Install Node.js and npm, then set up the project:

bashmkdir typescript-redis-lab
cd typescript-redis-lab
npm init -y
npm install -D typescript ts-node @types/node
npm install redis
npx tsc --init

Ensure Redis is running locally on port 6379, or use a cloud Redis instance.northflank+1

Project Structure

texttypescript-redis-lab/
├── src/
│   ├── types.ts          # Branded types and interfaces
│   ├── models.ts         # Domain models
│   ├── redis-client.ts   # Redis connection
│   ├── repository.ts     # Data access layer
│   └── index.ts          # Main application
├── package.json
└── tsconfig.json

Step 1: Define Branded Types and Interfaces

Create src/types.ts to establish type-safe domain primitives:pulkitxm+1

typescript// Branded type pattern
type Brand<T, B> = T & { __brand: B };

// Branded ID types prevent mixing different entity IDs
export type UserId = Brand<string, "UserId">;
export type ProductId = Brand<string, "ProductId">;
export type OrderId = Brand<string, "OrderId">;

// Helper functions to create branded types
export function createUserId(id: string): UserId {
    return id as UserId;
}

export function createProductId(id: string): ProductId {
    return id as ProductId;
}

export function createOrderId(id: string): OrderId {
    return id as OrderId;
}

// Domain interfaces
export interface User {
    id: UserId;
    name: string;
    email: string;
    createdAt: Date;
}

export interface Product {
    id: ProductId;
    name: string;
    price: number;
    inStock: boolean;
}

export interface Order {
    id: OrderId;
    userId: UserId;
    productIds: ProductId[];
    total: number;
    status: "pending" | "shipped" | "delivered";
}

The branded types prevent compile-time errors like accidentally passing a ProductId to a function expecting a UserId.madewithlove+1

Step 2: Create Domain Models

Create src/models.ts to define classes with business logic:geeksforgeeks+1

typescriptimport { User, UserId, Product, ProductId, Order, OrderId } from './types';

export class UserModel implements User {
    constructor(
        public id: UserId,
        public name: string,
        public email: string,
        public createdAt: Date = new Date()
    ) {}

    // Business logic methods
    getDisplayName(): string {
        return `${this.name} <${this.email}>`;
    }

    // Convert to plain object for Redis storage
    toJSON(): object {
        return {
            id: this.id,
            name: this.name,
            email: this.email,
            createdAt: this.createdAt.toISOString()
        };
    }

    // Create from plain object (from Redis)
    static fromJSON(data: any): UserModel {
        return new UserModel(
            data.id as UserId,
            data.name,
            data.email,
            new Date(data.createdAt)
        );
    }
}

export class ProductModel implements Product {
    constructor(
        public id: ProductId,
        public name: string,
        public price: number,
        public inStock: boolean = true
    ) {}

    getFormattedPrice(): string {
        return `$${this.price.toFixed(2)}`;
    }

    toJSON(): object {
        return {
            id: this.id,
            name: this.name,
            price: this.price,
            inStock: this.inStock
        };
    }

    static fromJSON(data: any): ProductModel {
        return new ProductModel(
            data.id as ProductId,
            data.name,
            data.price,
            data.inStock
        );
    }
}

export class OrderModel implements Order {
    constructor(
        public id: OrderId,
        public userId: UserId,
        public productIds: ProductId[],
        public total: number,
        public status: "pending" | "shipped" | "delivered" = "pending"
    ) {}

    ship(): void {
        this.status = "shipped";
    }

    deliver(): void {
        this.status = "delivered";
    }

    toJSON(): object {
        return {
            id: this.id,
            userId: this.userId,
            productIds: this.productIds,
            total: this.total,
            status: this.status
        };
    }

    static fromJSON(data: any): OrderModel {
        return new OrderModel(
            data.id as OrderId,
            data.userId as UserId,
            data.productIds as ProductId[],
            data.total,
            data.status
        );
    }
}

Step 3: Setup Redis Connection

Create src/redis-client.ts for the Redis connection:dev+2

typescriptimport { createClient } from 'redis';

export type RedisClientType = ReturnType<typeof createClient>;

let redisClient: RedisClientType | null = null;

export async function getRedisClient(): Promise<RedisClientType> {
    if (!redisClient) {
        redisClient = createClient({
            url: 'redis://localhost:6379'
        });

        redisClient.on('error', (err) => {
            console.error('Redis Client Error:', err);
        });

        redisClient.on('connect', () => {
            console.log('Connected to Redis successfully');
        });

        await redisClient.connect();
    }

    return redisClient;
}

export async function closeRedisClient(): Promise<void> {
    if (redisClient) {
        await redisClient.quit();
        redisClient = null;
    }
}

This singleton pattern ensures only one Redis connection exists throughout the application lifecycle.[github]​

Step 4: Build the Repository Layer

Create src/repository.ts to handle data persistence:stackoverflow+1

typescriptimport { getRedisClient, RedisClientType } from './redis-client';
import { UserModel, ProductModel, OrderModel } from './models';
import { UserId, ProductId, OrderId } from './types';

export class Repository<T, ID> {
    private client: RedisClientType | null = null;

    constructor(private prefix: string) {}

    private async getClient(): Promise<RedisClientType> {
        if (!this.client) {
            this.client = await getRedisClient();
        }
        return this.client;
    }

    private getKey(id: ID): string {
        return `${this.prefix}:${id}`;
    }

    async save(id: ID, entity: T): Promise<void> {
        const client = await this.getClient();
        const key = this.getKey(id);
        const value = JSON.stringify(entity);
        await client.set(key, value);
    }

    async findById(id: ID): Promise<string | null> {
        const client = await this.getClient();
        const key = this.getKey(id);
        return await client.get(key);
    }

    async findAll(): Promise<string[]> {
        const client = await this.getClient();
        const pattern = `${this.prefix}:*`;
        const keys = await client.keys(pattern);
        
        if (keys.length === 0) {
            return [];
        }

        const values = await client.mGet(keys);
        return values.filter((v): v is string => v !== null);
    }

    async delete(id: ID): Promise<boolean> {
        const client = await this.getClient();
        const key = this.getKey(id);
        const result = await client.del(key);
        return result > 0;
    }
}

// Type-specific repositories
export class UserRepository extends Repository<UserModel, UserId> {
    constructor() {
        super('user');
    }

    async findUserById(id: UserId): Promise<UserModel | null> {
        const data = await this.findById(id);
        if (!data) return null;
        return UserModel.fromJSON(JSON.parse(data));
    }

    async getAllUsers(): Promise<UserModel[]> {
        const dataArray = await this.findAll();
        return dataArray.map(data => UserModel.fromJSON(JSON.parse(data)));
    }
}

export class ProductRepository extends Repository<ProductModel, ProductId> {
    constructor() {
        super('product');
    }

    async findProductById(id: ProductId): Promise<ProductModel | null> {
        const data = await this.findById(id);
        if (!data) return null;
        return ProductModel.fromJSON(JSON.parse(data));
    }

    async getAllProducts(): Promise<ProductModel[]> {
        const dataArray = await this.findAll();
        return dataArray.map(data => ProductModel.fromJSON(JSON.parse(data)));
    }
}

export class OrderRepository extends Repository<OrderModel, OrderId> {
    constructor() {
        super('order');
    }

    async findOrderById(id: OrderId): Promise<OrderModel | null> {
        const data = await this.findById(id);
        if (!data) return null;
        return OrderModel.fromJSON(JSON.parse(data));
    }

    async getAllOrders(): Promise<OrderModel[]> {
        const dataArray = await this.findAll();
        return dataArray.map(data => OrderModel.fromJSON(JSON.parse(data)));
    }
}

This repository pattern demonstrates array operations and type casting—retrieving JSON strings from Redis and casting them back to domain models.logrocket+2

Step 5: Create the Main Application

Create src/index.ts with hardcoded test data:typestrong+1

typescriptimport { closeRedisClient } from './redis-client';
import { UserRepository, ProductRepository, OrderRepository } from './repository';
import { UserModel, ProductModel, OrderModel } from './models';
import { createUserId, createProductId, createOrderId } from './types';

async function main() {
    console.log('=== TypeScript Redis Object Management Lab ===\n');

    // Initialize repositories
    const userRepo = new UserRepository();
    const productRepo = new ProductRepository();
    const orderRepo = new OrderRepository();

    // Create users with branded IDs
    const user1 = new UserModel(
        createUserId('user-001'),
        'Alice Johnson',
        'alice@example.com'
    );

    const user2 = new UserModel(
        createUserId('user-002'),
        'Bob Smith',
        'bob@example.com'
    );

    // Create products
    const product1 = new ProductModel(
        createProductId('prod-001'),
        'TypeScript Course',
        49.99,
        true
    );

    const product2 = new ProductModel(
        createProductId('prod-002'),
        'Redis Masterclass',
        39.99,
        true
    );

    // Save users to Redis
    console.log('--- Saving Users ---');
    await userRepo.save(user1.id, user1);
    await userRepo.save(user2.id, user2);
    console.log(`Saved: ${user1.getDisplayName()}`);
    console.log(`Saved: ${user2.getDisplayName()}\n`);

    // Save products to Redis
    console.log('--- Saving Products ---');
    await productRepo.save(product1.id, product1);
    await productRepo.save(product2.id, product2);
    console.log(`Saved: ${product1.name} - ${product1.getFormattedPrice()}`);
    console.log(`Saved: ${product2.name} - ${product2.getFormattedPrice()}\n`);

    // Create and save an order
    const order1 = new OrderModel(
        createOrderId('order-001'),
        user1.id,
        [product1.id, product2.id],
        89.98
    );

    console.log('--- Saving Order ---');
    await orderRepo.save(order1.id, order1);
    console.log(`Saved Order ${order1.id} for User ${order1.userId}\n`);

    // Retrieve and display all users
    console.log('--- Retrieving All Users ---');
    const allUsers = await userRepo.getAllUsers();
    allUsers.forEach(user => {
        console.log(`${user.id}: ${user.getDisplayName()}`);
    });

    // Retrieve specific product
    console.log('\n--- Retrieving Specific Product ---');
    const retrievedProduct = await productRepo.findProductById(createProductId('prod-001'));
    if (retrievedProduct) {
        console.log(`Found: ${retrievedProduct.name} - ${retrievedProduct.getFormattedPrice()}`);
    }

    // Update order status
    console.log('\n--- Updating Order Status ---');
    const retrievedOrder = await orderRepo.findOrderById(createOrderId('order-001'));
    if (retrievedOrder) {
        console.log(`Current status: ${retrievedOrder.status}`);
        retrievedOrder.ship();
        await orderRepo.save(retrievedOrder.id, retrievedOrder);
        console.log(`Updated status: ${retrievedOrder.status}`);
    }

    // Demonstrate type safety with branded types
    console.log('\n--- Type Safety Demonstration ---');
    // This would cause a compile error:
    // await userRepo.findUserById(product1.id); // Error: ProductId not assignable to UserId
    console.log('✓ Branded types prevent ID mix-ups at compile time');

    // Retrieve all orders
    console.log('\n--- Retrieving All Orders ---');
    const allOrders = await orderRepo.getAllOrders();
    allOrders.forEach(order => {
        console.log(`Order ${order.id}: ${order.productIds.length} products, Total: $${order.total}, Status: ${order.status}`);
    });

    // Cleanup
    console.log('\n--- Closing Redis Connection ---');
    await closeRedisClient();
    console.log('Connection closed successfully');
}

// Run the application
main().catch(error => {
    console.error('Application error:', error);
    process.exit(1);
});

Step 6: Configure TypeScript

Update tsconfig.json for optimal ts-node execution:[blog.logrocket]​

json{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "moduleResolution": "node"
  },
  "ts-node": {
    "transpileOnly": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Running the Lab

Execute the application directly with ts-node:[youtube]​[github]​

bashnpx ts-node src/index.ts

The output demonstrates object creation, Redis persistence, retrieval with type casting, and the type safety provided by branded types.

Key Takeaways

This lab integrates multiple TypeScript concepts into a cohesive application:

  • Branded types ensure domain integrity by preventing ID mix-ups at compile timepulkitxm+1
  • Object-oriented design with classes and interfaces provides clear structure and business logic encapsulationtypescriptlang+1
  • Type casting from Redis JSON strings back to domain models maintains type safety throughout the data floww3schools+1
  • Redis integration provides fast, persistent storage for TypeScript objects using JSON serializationgithub+1
  • ts-node workflow eliminates build steps during development, letting you focus on code rather than toolinggithub+1

Next Steps

Extend this lab by:

  • Adding validation with Zod schemas
  • Implementing Redis Hash storage for faster field access[redis]​
  • Creating indexes for complex queries
  • Adding error handling and retry logic
  • Building a REST API layer with Express
  • Implementing Redis pub/sub for real-time updates

This foundation prepares you for production TypeScript applications with robust persistence and type safety throughout your stack.