{"id":30581633,"url":"https://github.com/yoanesber/typescript-idempotency-with-redis","last_synced_at":"2026-05-14T23:08:14.007Z","repository":{"id":306010641,"uuid":"1020366330","full_name":"yoanesber/TypeScript-Idempotency-with-Redis","owner":"yoanesber","description":"TypeScript API demo using Redis to ensure idempotency in POST/PUT ops, avoiding duplicate processing from retries or refreshes.","archived":false,"fork":false,"pushed_at":"2025-07-28T14:37:37.000Z","size":59,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-29T08:59:58.274Z","etag":null,"topics":["idempotency","redis","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yoanesber.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-15T18:53:05.000Z","updated_at":"2025-07-28T14:37:41.000Z","dependencies_parsed_at":"2025-07-23T07:15:52.140Z","dependency_job_id":"aaaab779-faa5-4706-be1c-b569b02ac419","html_url":"https://github.com/yoanesber/TypeScript-Idempotency-with-Redis","commit_stats":null,"previous_names":["yoanesber/typescript-idempotency-with-redis"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/yoanesber/TypeScript-Idempotency-with-Redis","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoanesber%2FTypeScript-Idempotency-with-Redis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoanesber%2FTypeScript-Idempotency-with-Redis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoanesber%2FTypeScript-Idempotency-with-Redis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoanesber%2FTypeScript-Idempotency-with-Redis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yoanesber","download_url":"https://codeload.github.com/yoanesber/TypeScript-Idempotency-with-Redis/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoanesber%2FTypeScript-Idempotency-with-Redis/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":275852425,"owners_count":25540137,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-18T02:00:09.552Z","response_time":77,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["idempotency","redis","typescript"],"created_at":"2025-08-29T06:44:09.595Z","updated_at":"2025-09-18T23:43:26.923Z","avatar_url":"https://github.com/yoanesber.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Idempotency Demo with Redis\n\nThis project demonstrates implementing an idempotency mechanism in a **TypeScript-based API** using **Express** and **Redis**. It ensures duplicate requests (from retries, refreshes, etc.) do not cause repeated processing, especially in `POST`/`PUT` operations like payments or transactions.\n\n---\n\n## 🔁 Flow\n\nBelow is the high-level flow describing how the Idempotency Middleware and Transaction Service work together to ensure safe and reliable processing of requests:\n\n```text\n┌──────────────────────────────────────────────┐\n│            [1] Client Sends Request          │\n│----------------------------------------------│\n│ - POST /transactions                         │\n│ - Headers:                                   │\n│   - Idempotency-Key: \u003cUUID\u003e                  │\n│ - Body: { type, amount, consumerId }         │\n└──────────────────────────────────────────────┘\n              │\n              ▼\n┌──────────────────────────────────────────────┐\n│     [2] Middleware: Validate Idempotency     │\n│----------------------------------------------│\n│ - Ensure Idempotency-Key is present          │\n│ - Hash request body using SHA256             │\n└──────────────────────────────────────────────┘\n              │\n              ▼\n┌──────────────────────────────────────────────┐\n│     [3] Redis Lookup: Cached Response?       │\n│----------------------------------------------│\n│ - If found →                                 │\n│     - Compare stored bodyHash                │\n│       - Mismatch → reject (409 Conflict)     │\n│       - Match →                              │\n│         - Check expiredAt (in payload)       │\n│           - If expired → reject (419 Expired)│\n│           - If valid   → return 200          │\n│ - If not found → check PostgreSQL DB [4]     │\n└──────────────────────────────────────────────┘\n              │\n              ▼\n┌──────────────────────────────────────────────┐\n│ [4] DB Lookup: Idempotency Record Exists?    │\n│----------------------------------------------│\n│ - If found →                                 │\n│     - Compare stored bodyHash with current   │\n│       - Mismatch → reject (409 Conflict)     │\n│       - Match →                              │\n│         - If expired → reject (419 Expired)  │\n│         - If valid   → return 200            │\n│ - If not found → proceed to service [5]      │\n└──────────────────────────────────────────────┘\n              │\n              ▼\n┌────────────────────────────────────────────────┐\n│    [5] Service Layer Processing                │\n│------------------------------------------------│\n│ - Validate and parse request body              │\n│ - Destructure the validated data               │\n│ - Begin database transaction                   │\n│   - Create transaction with status = \"pending\" │\n│   - Create idempotency_meta with:              │\n│     (key, bodyHash, responsePayload, expiredAt)│\n│   - Save response to Redis (short TTL)         │\n│ - Commit transaction                           │\n└────────────────────────────────────────────────┘\n              │\n              ▼\n┌──────────────────────────────────────────────┐\n│        [6] API Response Handler              │\n│----------------------------------------------│\n│ - HTTP 201 Created                           │\n│ - Body: { transactionId, status, ... }       │\n└──────────────────────────────────────────────┘\n```\n\n---\n\n\n## 🤖 Tech Stack\n\nThis project leverages a modern and robust **Node.js-based** stack to implement an idempotent-safe REST API using Redis as a caching mechanism. Below is an overview of the core technologies and tools used:\n\n| **Component**          | **Description**                                                                                          |\n|------------------------|----------------------------------------------------------------------------------------------------------|\n| Language               | **TypeScript** — statically typed superset of JavaScript for safer and scalable development              |\n| Runtime                | **Node.js** — JavaScript runtime built on Chrome’s V8 engine                                             |\n| Web Framework          | **Express.js** — minimalist and flexible web application framework                                       |\n| Caching Layer          | **Redis** — in-memory data structure store used for idempotency key tracking and response caching        |\n| Idempotency Logic      | **Custom Middleware** — detects repeated requests and replays cached responses using key + body hash     |\n| Request Hashing        | **Crypto (SHA256)** — used to hash request bodies to detect changes in content                           |\n| Environment Config     | **dotenv** — loads environment variables from `.env` file into `process.env`                             |\n| Rate Limiting          | **express-rate-limit** — limits repeated requests to APIs to prevent abuse                               |\n| Validation             | **Zod** — TypeScript-first schema declaration and validation library                                     |\n| Migration \u0026 Seeding    | **Sequelize CLI** — for database schema generation and initial data population                           |\n| Containerization       | **Docker** — optional, Redis can be containerized for local testing and production deployment            |\n\n\n---\n\n## 🧱 Architecture Overview\n\nThe project follows a modular and layered folder structure for maintainability, scalability, and separation of concerns. Below is a high-level overview of the folder architecture:\n\n```\n📁typescript-idempotency-demo/\n├── 📁docker/\n│   ├── 📁app/                # Dockerfile and setup for Node.js app container\n│   ├── 📁postgres/           # PostgreSQL Docker setup with init scripts or volumes\n│   └── 📁redis/              # Redis Docker setup\n├── 📁logs/                   # Directory for application and HTTP logs\n├── 📁migrations/             # Sequelize migrations\n├── 📁src/                    # Application source code\n│   ├── 📁config/             # Configuration files (DB, environment, Sequelize)\n│   ├── 📁controllers/        # Express route handlers, business logic endpoints\n│   ├── 📁dtos/               # Data Transfer Objects for validation and typing\n│   ├── 📁exceptions/         # Custom error classes for centralized error handling\n│   ├── 📁middlewares/        # Express middlewares (security, logging, rate limiters, etc.)\n│   ├── 📁models/             # Sequelize models representing DB entities\n│   ├── 📁routes/             # API route definitions and registration\n│   ├── 📁services/           # Business logic and service layer between controllers and models\n│   ├── 📁types/              # Custom global TypeScript type definitions\n│   └── 📁utils/              # Utility functions (e.g., redis operations, logger)\n├── .env                    # Environment variables for configuration (DB credentials, Redis, Idempotency settings)\n├── .sequelizerc            # Sequelize CLI configuration\n├── entrypoint.sh           # Script executed at container startup (wait-for-db, run migrations, start app)\n├── package.json            # Node.js project metadata and scripts\n├── sequelize.config.js     # Wrapper to load TypeScript Sequelize config via ts-node\n├── tsconfig.json           # TypeScript compiler configuration\n└── README.md               # Project documentation\n```\n\n---\n\n## 🛠️ Installation \u0026 Setup  \n\nFollow the instructions below to get the project up and running in your local development environment. You may run it natively or via Docker depending on your preference.  \n\n### ✅ Prerequisites\n\nMake sure the following tools are installed on your system:\n\n| **Tool**                                                    | **Description**                                    |\n|-------------------------------------------------------------|----------------------------------------------------|\n| [Node.js](https://nodejs.org/)                              | JavaScript runtime environment (v20+)              |\n| [npm](https://www.npmjs.com/)                               | Node.js package manager (bundled with Node.js)     |\n| [Make](https://www.gnu.org/software/make/)                  | Build automation tool (`make`)                     |\n| [PostgreSQL](https://www.postgresql.org/)                   | Relational database system (v14+)                  |\n| [Redis](https://redis.io/)                                  | In-memory data structure store (v7+)               |\n| [Docker](https://www.docker.com/)                           | Containerization platform (optional)               |\n\n### 🔁 Clone the Project  \n\nClone the repository:  \n\n```bash\ngit clone https://github.com/yoanesber/TypeScript-Idempotency-with-Redis.git\ncd TypeScript-Idempotency-with-Redis\n```\n\n### ⚙️ Configure `.env` File  \n\nSet up your **database** and **Redis** configurations by creating a `.env` file in the project root directory:\n\n```properties\n# Application Configuration\nPORT=4000\n# development, production, test\nNODE_ENV=development\n\n# Logging Configuration\nLOG_LEVEL=info\nLOG_DIRECTORY=../../logs\n\n# Postgre Configuration\nDB_HOST=localhost\nDB_PORT=5432\nDB_USER=postgres\nDB_PASS=P@ssw0rd\nDB_NAME=nodejs_demo\nDB_DIALECT=postgres\n\n# Redis configuration\nREDIS_HOST=localhost\nREDIS_PORT=6379\nREDIS_USER=default\nREDIS_PASS=\nREDIS_DB=0\nREDIS_FLUSH_DB=TRUE\nREDIS_CONNECT_TIMEOUT=10000 # 10 seconds\n\n# Idempotency configuration\nIDEMPOTENCY_ENABLED=TRUE\nIDEMPOTENCY_HEADER_NAME=idempotency-key\nIDEMPOTENCY_PREFIX=idempotency\nIDEMPOTENCY_TTL_HOURS=24\n```\n\n### 👤 Create Dedicated PostgreSQL User (Recommended)\n\nFor security reasons, it's recommended to avoid using the default postgres superuser. Use the following SQL script to create a dedicated user (`appuser`) and assign permissions:\n\n```sql\n-- Create appuser and database\nCREATE USER appuser WITH PASSWORD 'app@123';\n\n-- Allow user to connect to database\nGRANT CONNECT, TEMP, CREATE ON DATABASE nodejs_demo TO appuser;\n\n-- Grant permissions on public schema\nGRANT USAGE, CREATE ON SCHEMA public TO appuser;\n\n-- Grant all permissions on existing tables\nGRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO appuser;\n\n-- Grant all permissions on sequences (if using SERIAL/BIGSERIAL ids)\nGRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO appuser;\n\n-- Ensure future tables/sequences will be accessible too\nALTER DEFAULT PRIVILEGES IN SCHEMA public\nGRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO appuser;\n\n-- Ensure future sequences will be accessible too\nALTER DEFAULT PRIVILEGES IN SCHEMA public\nGRANT USAGE, SELECT, UPDATE ON SEQUENCES TO appuser;\n```\n\nUpdate your `.env` accordingly:\n```properties\nDB_USER=appuser\nDB_PASS=app@123\n```\n\n---\n\n\n## 🚀 Running the Application  \n\nThis section provides step-by-step instructions to run the application either **locally** or via **Docker containers**.\n\n- **Notes**:  \n  - All commands are defined in the `Makefile`.\n  - To run using `make`, ensure that `make` is installed on your system.\n  - To run the application in containers, make sure `Docker` is installed and running.\n  - Ensure you have `NodeJs` and `npm` installed on your system\n\n### 📦 Install Dependencies\n\nMake sure all dependencies are properly installed:  \n\n```bash\nmake install\n```\n\n### 🔧 Run Locally (Non-containerized)\n\nEnsure PostgreSQL and Redis are running locally, then:\n\n```bash\nmake dev\n```\n\nThis command will run the application in development mode, listening on port `4000` by default.\n\n### Run Migrations\n\nTo create the database schema, run:\n\n```bash\nmake refresh-migrate\n```\n\nThis will apply all pending migrations to your PostgreSQL database.\n\n### 🐳 Run Using Docker\n\nTo build and run all services (PostgreSQL, Redis, and TypeScript app):\n\n```bash\nmake docker-up\n```\n\nTo stop and remove all containers:\n\n```bash\nmake docker-down\n```\n\n- **Notes**:  \n  - Before running the application inside Docker, make sure to update your environment variables `.env`\n    - Change `DB_HOST=localhost` to `DB_HOST=postgres-server`.\n    - Change `REDIS_HOST=localhost` to `REDIS_HOST=redis-server`.\n\n### 🟢 Application is Running\n\nNow your application is accessible at:\n```bash\nhttp://localhost:4000\n```\n\n---\n\n## 🧪 Testing Scenarios  \n\nThis section outlines various test scenarios to validate the idempotency mechanism and middleware functionalities. Each scenario includes the request method, endpoint, preconditions, expected responses, and any relevant headers.\n\n### 🔄 Idempotency Key Usage\n#### Scenario 1: Create a Payment with Idempotency Key\n- **Method**: `POST`\n- **Endpoint**: `/api/transactions`\n- **Request Body**:\n```json\n{\n    \"type\": \"payment\",\n    \"amount\": 12000.00,\n    \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\"\n}\n```\n- **Headers**:\n```http\nContent-Type: application/json\nIdempotency-Key: unique-key-123\n```\n- **Expected Response**: Get `201 Created` response with transaction details.\n```json\n{\n    \"message\": \"Transaction created successfully\",\n    \"error\": null,\n    \"data\": {\n        \"id\": \"8c825416-f74d-4a9d-aea6-9b5c82c5d22b\",\n        \"type\": \"payment\",\n        \"amount\": \"12000.00\",\n        \"status\": \"pending\",\n        \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\",\n        \"createdAt\": \"2025-07-08T19:03:12.741Z\",\n        \"updatedAt\": \"2025-07-08T19:03:12.741Z\"\n    },\n    \"path\": \"/api/transactions\",\n    \"timestamp\": \"2025-07-08T19:03:12.754Z\"\n}\n```\n\n#### Scenario 2: Retry the Same Payment Request\n- **Method**: `POST`\n- **Endpoint**: `/api/transactions`\n- **Request Body**:\n```json\n{\n    \"type\": \"payment\",\n    \"amount\": 12000.00,\n    \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\"\n}\n```\n- **Headers**:\n```http\nContent-Type: application/json\nIdempotency-Key: unique-key-123\n```\n- **Expected Response**: Get `200 OK` response with the same transaction details, indicating the request was idempotent and already processed.\n```json\n{\n    \"message\": \"Transaction already processed\",\n    \"error\": null,\n    \"data\": {\n        \"id\": \"8c825416-f74d-4a9d-aea6-9b5c82c5d22b\",\n        \"type\": \"payment\",\n        \"amount\": \"12000.00\",\n        \"status\": \"pending\",\n        \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\",\n        \"createdAt\": \"2025-07-08T19:03:12.741Z\",\n        \"updatedAt\": \"2025-07-08T19:03:12.741Z\"\n    },\n    \"path\": \"/api/transactions\",\n    \"timestamp\": \"2025-07-08T19:04:40.975Z\"\n}\n```\n\n\n#### Scenario 3: Create a Payment with a Different Body but Same Idempotency Key\n- **Method**: `POST`\n- **Endpoint**: `/api/transactions`\n- **Request Body**:\n```json\n{\n    \"type\": \"payment\",\n    \"amount\": 15000.00,\n    \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\"\n}\n```\n- **Headers**:\n```http\nContent-Type: application/json\nIdempotency-Key: unique-key-123\n```\n- **Expected Response**: Get `409 Conflict` response indicating idempotency key conflict.\n```json\n{\n    \"message\": \"Idempotency key conflict\",\n    \"error\": \"A transaction with this idempotency key already exists with a different request body.\",\n    \"data\": null,\n    \"path\": \"/api/transactions\",\n    \"timestamp\": \"2025-07-08T19:07:48.718Z\"\n}\n```\n\n#### Scenario 4: Create a Payment with a Different Idempotency Key\n- **Method**: `POST`\n- **Endpoint**: `/api/transactions`\n- **Request Body**:\n```json\n{\n    \"type\": \"payment\",\n    \"amount\": 12000.00,\n    \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\"\n}\n```\n- **Headers**:\n```http\nContent-Type: application/json\nIdempotency-Key: unique-key-456\n```\n- **Expected Response**: Get `201 Created` response with new transaction details, indicating a new request was processed successfully.\n```json\n{\n    \"message\": \"Transaction created successfully\",\n    \"error\": null,\n    \"data\": {\n        \"id\": \"46f704f5-cfd6-43b8-8a3f-563f0a97ccea\",\n        \"type\": \"payment\",\n        \"amount\": \"12000.00\",\n        \"status\": \"pending\",\n        \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\",\n        \"createdAt\": \"2025-07-08T19:05:23.029Z\",\n        \"updatedAt\": \"2025-07-08T19:05:23.029Z\"\n    },\n    \"path\": \"/api/transactions\",\n    \"timestamp\": \"2025-07-08T19:05:23.048Z\"\n}\n```\n\n#### Scenario 5: Create a Payment with No Idempotency Key\n- **Method**: `POST`\n- **Endpoint**: `/api/transactions`\n- **Request Body**:\n```json\n{\n    \"type\": \"payment\",\n    \"amount\": 12000.00,\n    \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\"\n}\n```\n- **Headers**:\n```http\nContent-Type: application/json\n```\n- **Expected Response**: Get `400 Bad Request` response indicating the idempotency key is required.\n```json\n{\n    \"message\": \"Invalid idempotency key\",\n    \"error\": \"Idempotency key is required and must be a non-empty string\",\n    \"data\": null,\n    \"path\": \"/api/transactions\",\n    \"timestamp\": \"2025-07-08T19:10:17.104Z\"\n}\n```\n\n#### Scenario 6: Create a Payment with Invalid Idempotency Key\n- **Method**: `POST`\n- **Endpoint**: `/api/transactions`\n- **Request Body**:\n```json\n{\n    \"type\": \"payment\",\n    \"amount\": 12000.00,\n    \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\"\n}\n```\n- **Headers**:\n```http\nContent-Type: application/json\nIdempotency-Key: \"\"\n```\n- **Expected Response**: Get `400 Bad Request` response indicating the idempotency key is invalid.\n```json\n{\n    \"message\": \"Invalid idempotency key\",\n    \"error\": \"Idempotency key is required and must be a non-empty string\",\n    \"data\": null,\n    \"path\": \"/api/transactions\",\n    \"timestamp\": \"2025-07-08T19:11:09.380Z\"\n}\n```\n\n#### Scenario 7: Create a Payment with Expired Idempotency Key (after TTL)\n- **Method**: `POST`\n- **Endpoint**: `/api/transactions`\n- **Request Body**:\n```json\n{\n    \"type\": \"payment\",\n    \"amount\": 12000.00,\n    \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\"\n}\n```\n- **Headers**:\n```http\nContent-Type: application/json\nIdempotency-Key: \"unique-key-123\"\n```\n- **Expected Response**: Get `419 Expired` response indicating the idempotency key has expired.\n```json\n{\n    \"message\": \"Idempotency key expired\",\n    \"error\": \"The idempotency key has expired and cannot be used for this transaction.\",\n    \"data\": null,\n    \"path\": \"/api/transactions\",\n    \"timestamp\": \"2025-07-08T19:18:51.810Z\"\n}\n```\n\n\n### 🔍 Fetch All Transactions\n\n#### Scenario 1: Fetch All Transactions\n- **Method**: `GET`\n- **Endpoint**: `/api/transactions`\n- **Expected Response**: Get `200 OK` response with a list of all transactions.\n```json\n{\n    \"message\": \"Transactions fetched successfully\",\n    \"error\": null,\n    \"data\": [\n        {\n            \"id\": \"39212e91-b52f-4eb0-b15c-0bec7d46e818\",\n            \"type\": \"payment\",\n            \"amount\": \"12000.00\",\n            \"status\": \"pending\",\n            \"consumerId\": \"2e373ce7-7207-43a4-9133-c820253252f6\",\n            \"createdAt\": \"2025-07-08T19:20:25.730Z\",\n            \"updatedAt\": \"2025-07-08T19:20:25.730Z\"\n        }\n    ],\n    \"path\": \"/api/transactions?page=1\u0026limit=10\u0026sortBy=createdAt\u0026sortOrder=desc\",\n    \"timestamp\": \"2025-07-08T19:20:30.907Z\"\n}\n```\n\n---\n\n\n## 📘 API Documentation  \n\nThe API is documented using **Swagger (OpenAPI `3.0`)**. You can explore and test API endpoints directly from the browser using Swagger UI at:\n\n```\nhttp://localhost:4000/api-docs\n```\n\nThis provides an interactive interface to explore the API endpoints, request/response formats, and available operations.\n\nOpenAPI Spec can be found in  [`swagger.yaml`](./swagger.yaml) file.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyoanesber%2Ftypescript-idempotency-with-redis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyoanesber%2Ftypescript-idempotency-with-redis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyoanesber%2Ftypescript-idempotency-with-redis/lists"}