A TypeScript demo application showing how to implement idempotency keys to make APIs safer and more reliable.
src/
├── __tests__/ # Unit tests
│ ├── setup.ts # Test configuration
│ ├── services/ # Service layer tests
├── config/ # Configuration files
│ └── database.ts # TypeORM database configuration
├── controllers/ # Request handlers
│ └── order.controller.ts # Order-related endpoints
├── entities/ # TypeORM entities
│ ├── order.entity.ts # Order entity
│ └── idempotency-record.entity.ts # Idempotency tracking entity
├── middleware/ # Express middleware
│ └── idempotency.middleware.ts # Idempotency key validation
├── routes/ # API routes
│ └── order.routes.ts # Order endpoints
├── services/ # Business logic
│ ├── order.service.ts # Order operations
│ └── idempotency.service.ts # Idempotency management
├── types/ # TypeScript type definitions
│ └── index.ts # Shared types
├── app.ts # Express app configuration
└── index.ts # Application entry point
POST /api/v1/orders/non-idempotent- Demonstrates the problem- Multiple identical requests create duplicate orders
POST /api/v1/orders- The solution- Requires
Idempotency-Keyheader (UUID format) - Prevents duplicate operations
- Returns cached responses for repeat requests
- Detects payload conflicts
- Node.js 18+
- npm or yarn
npm install# Development mode
npm run dev# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coveragecurl -X POST http://localhost:3000/api/v1/orders/non-idempotent \
-H "Content-Type: application/json" \
-d '{
"customerEmail": "[email protected]",
"productId": "prod-123",
"amount": 99.99,
"currency": "USD"
}'curl -X POST http://localhost:3000/api/v1/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"customerEmail": "[email protected]",
"productId": "prod-123",
"amount": 99.99,
"currency": "USD"
}'curl http://localhost:3000/api/v1/orders/{order-id}First request creates an order:
curl -X POST http://localhost:3000/api/v1/orders \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001" \
-H "Content-Type: application/json" \
-d '{"customerEmail": "[email protected]", "productId": "prod-123", "amount": 99.99, "currency": "USD"}'Second request returns the saved response:
curl -X POST http://localhost:3000/api/v1/orders \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001" \
-H "Content-Type: application/json" \
-d '{"customerEmail": "[email protected]", "productId": "prod-123", "amount": 99.99, "currency": "USD"}'First request:
curl -X POST http://localhost:3000/api/v1/orders \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{"customerEmail": "[email protected]", "productId": "prod-123", "amount": 99.99, "currency": "USD"}'Second request with different payload (and the same idempotency key) returns 409 Conflict:
curl -X POST http://localhost:3000/api/v1/orders \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{"customerEmail": "[email protected]", "productId": "prod-123", "amount": 199.99, "currency": "USD"}'The project includes CircleCI configuration for:
- Automated testing on every push
- TypeScript - Type-safe JavaScript
- Express.js - Web framework
- TypeORM - Object-Relational Mapping
- SQLite - Database (in-memory for tests)
- Jest - Testing framework
- Supertest - HTTP assertion library
- CircleCI - Continuous Integration