@appaka/redis
v0.1.1
Published
A Redis client with @upstash/redis API compatibility, powered by ioredis
Maintainers
Readme
@appaka/redis
A Redis client with @upstash/redis API compatibility, powered by ioredis.
Why?
If you're using @upstash/redis but want to connect to a standard Redis server (self-hosted, AWS ElastiCache, Redis Cloud, etc.) instead of Upstash's HTTP-based service, this library lets you do that with zero code changes.
| @upstash/redis | @appaka/redis | |----------------|---------------| | HTTP REST API | Native TCP protocol | | Higher latency | Lower latency | | Stateless | Persistent connections | | Upstash only | Any Redis server |
Installation
npm install @appaka/redis
# or
pnpm add @appaka/redis
# or
yarn add @appaka/redisQuick Start
import { Redis } from '@appaka/redis'
// Connect to Redis
const redis = new Redis({
host: 'localhost',
port: 6379,
password: 'your-password', // optional
})
// Use exactly like @upstash/redis
await redis.set('key', 'value')
const value = await redis.get('key')
// Automatic JSON serialization
await redis.set('user', { name: 'Alice', age: 30 })
const user = await redis.get('user') // { name: 'Alice', age: 30 }Migration from @upstash/redis
Simply change the import and configuration:
- import { Redis } from '@upstash/redis'
+ import { Redis } from '@appaka/redis'
- const redis = new Redis({
- url: 'https://your-instance.upstash.io',
- token: 'your-token',
- })
+ const redis = new Redis({
+ host: 'localhost',
+ port: 6379,
+ })
// All your code works unchanged
await redis.set('key', 'value')
await redis.get('key')Upstash-style URL (also supported)
const redis = new Redis({
url: 'redis://localhost:6379',
})
// Or with authentication
const redis = new Redis({
url: 'redis://username:password@localhost:6379',
})Environment Variables
// Reads from UPSTASH_REDIS_REST_URL, REDIS_URL, or KV_REST_API_URL
const redis = Redis.fromEnv()Configuration
ioredis-style (recommended)
const redis = new Redis({
host: 'localhost',
port: 6379,
password: 'secret',
username: 'default', // Redis 6+ ACL
db: 0, // Database index
tls: {}, // TLS options for SSL
})Options
const redis = new Redis(
{ host: 'localhost', port: 6379 },
{
automaticDeserialization: true, // Auto JSON.parse responses (default: true)
}
)API Reference
String Commands
// Set a value
await redis.set('key', 'value')
await redis.set('key', 'value', { ex: 60 }) // Expire in 60 seconds
await redis.set('key', 'value', { px: 60000 }) // Expire in 60000 milliseconds
await redis.set('key', 'value', { nx: true }) // Only set if not exists
await redis.set('key', 'value', { xx: true }) // Only set if exists
// Get a value
const value = await redis.get('key') // Returns string or null
const user = await redis.get<User>('user') // With TypeScript generics
// Delete keys
const count = await redis.del('key1', 'key2') // Returns number deleted
// Check if keys exist
const exists = await redis.exists('key1', 'key2') // Returns count of existing
// Expiration
await redis.expire('key', 60) // Set TTL in seconds
const ttl = await redis.ttl('key') // Get TTL in seconds
// Increment/Decrement
await redis.incr('counter') // +1
await redis.incrby('counter', 5) // +5
await redis.incrbyfloat('counter', 1.5) // +1.5
await redis.decr('counter') // -1
await redis.decrby('counter', 5) // -5
// Multiple keys
const values = await redis.mget('key1', 'key2') // Get multiple values
await redis.mset({ key1: 'v1', key2: 'v2' }) // Set multiple key-values
// String operations
await redis.setex('key', 60, 'value') // Set with expiration
await redis.setnx('key', 'value') // Set if not exists
await redis.strlen('key') // Get string length
await redis.append('key', 'suffix') // Append to string
await redis.getrange('key', 0, 5) // Get substring
await redis.setrange('key', 0, 'new') // Replace substringHash Commands
// Set fields
await redis.hset('user:1', 'name', 'Alice')
await redis.hset('user:1', { name: 'Alice', age: 30 })
await redis.hmset('user:1', { name: 'Alice', age: 30 }) // Set multiple
await redis.hsetnx('user:1', 'email', '[email protected]') // Set if not exists
// Get fields
const name = await redis.hget('user:1', 'name')
const user = await redis.hgetall('user:1') // { name: 'Alice', age: 30 }
const vals = await redis.hmget('user:1', 'name', 'age') // Get multiple
const keys = await redis.hkeys('user:1') // Get all field names
const values = await redis.hvals('user:1') // Get all values
// Field operations
await redis.hexists('user:1', 'name') // Check field exists
await redis.hlen('user:1') // Number of fields
await redis.hstrlen('user:1', 'name') // Length of field value
await redis.hincrby('user:1', 'age', 1) // Increment integer
await redis.hincrbyfloat('user:1', 'score', 1.5) // Increment float
// Delete fields
await redis.hdel('user:1', 'age', 'email')
// Scan fields
const [cursor, fields] = await redis.hscan('user:1', 0, { match: 'n*' })List Commands
// Push values
await redis.lpush('list', 'a', 'b', 'c') // Push to left
await redis.rpush('list', 'x', 'y', 'z') // Push to right
await redis.lpushx('list', 'a') // Push only if list exists
await redis.rpushx('list', 'z') // Push only if list exists
// Pop values
const left = await redis.lpop('list') // Pop from left
const right = await redis.rpop('list') // Pop from right
// Get range
const items = await redis.lrange('list', 0, -1) // Get all items
const first3 = await redis.lrange('list', 0, 2) // Get first 3 items
// Element operations
const elem = await redis.lindex('list', 0) // Get by index
await redis.lset('list', 0, 'new') // Set by index
await redis.linsert('list', 'BEFORE', 'b', 'a') // Insert before element
await redis.linsert('list', 'AFTER', 'b', 'c') // Insert after element
const pos = await redis.lpos('list', 'b') // Find element position
// Remove/trim
await redis.lrem('list', 1, 'a') // Remove count occurrences
await redis.ltrim('list', 0, 99) // Trim to range
// Get length
const len = await redis.llen('list')Set Commands
// Add members
await redis.sadd('tags', 'redis', 'database', 'cache')
// Remove members
await redis.srem('tags', 'cache')
// Get all members
const tags = await redis.smembers('tags') // ['redis', 'database']
// Check membership
const isMember = await redis.sismember('tags', 'redis') // 1 or 0
const results = await redis.smismember('tags', 'redis', 'cache') // [1, 0]
// Random members
const random = await redis.srandmember('tags') // Get random member
const popped = await redis.spop('tags') // Remove and return random
// Set operations
const diff = await redis.sdiff('set1', 'set2') // Difference
const inter = await redis.sinter('set1', 'set2') // Intersection
const union = await redis.sunion('set1', 'set2') // Union
await redis.sdiffstore('result', 'set1', 'set2') // Store difference
await redis.sinterstore('result', 'set1', 'set2') // Store intersection
await redis.sunionstore('result', 'set1', 'set2') // Store union
await redis.smove('set1', 'set2', 'member') // Move member
// Get size
const size = await redis.scard('tags')
// Scan members
const [cursor, members] = await redis.sscan('tags', 0, { match: 'r*' })Sorted Set Commands
// Add members with scores
await redis.zadd('leaderboard',
{ score: 100, member: 'alice' },
{ score: 200, member: 'bob' },
)
// Get range by index
const top3 = await redis.zrange('leaderboard', 0, 2)
const withScores = await redis.zrange('leaderboard', 0, 2, { withScores: true })
const reversed = await redis.zrange('leaderboard', 0, 2, { rev: true })
// Get range by score
const byScore = await redis.zrange('leaderboard', 0, 100, { byScore: true })
// Get range by lex
const byLex = await redis.zrange('leaderboard', '[a', '[z', { byLex: true })
// Score operations
const score = await redis.zscore('leaderboard', 'alice') // 100
const scores = await redis.zmscore('leaderboard', 'alice', 'bob') // [100, 200]
await redis.zincrby('leaderboard', 10, 'alice') // Increment score
// Rank operations
const rank = await redis.zrank('leaderboard', 'alice') // 0-based rank
const revRank = await redis.zrevrank('leaderboard', 'alice') // Reverse rank
// Count and pop
const count = await redis.zcount('leaderboard', 0, 100) // Count in range
const lowest = await redis.zpopmin('leaderboard', 2) // Pop lowest
const highest = await redis.zpopmax('leaderboard', 2) // Pop highest
// Lex operations
const lexCount = await redis.zlexcount('leaderboard', '[a', '[z')
// Remove operations
await redis.zrem('leaderboard', 'alice')
await redis.zremrangebyrank('leaderboard', 0, 2) // Remove by rank
await redis.zremrangebyscore('leaderboard', 0, 100) // Remove by score
await redis.zremrangebylex('leaderboard', '[a', '[m') // Remove by lex
// Get size
const size = await redis.zcard('leaderboard')
// Scan members
const [cursor, members] = await redis.zscan('leaderboard', 0)Key Commands
// Key operations
const type = await redis.type('key') // Get key type
const keys = await redis.keys('user:*') // Find keys by pattern
await redis.rename('old', 'new') // Rename key
await redis.renamenx('old', 'new') // Rename if new doesn't exist
await redis.copy('source', 'dest') // Copy key
const random = await redis.randomkey() // Get random key
// Expiration
await redis.expire('key', 60) // Expire in seconds
await redis.expireat('key', 1234567890) // Expire at Unix timestamp
await redis.pexpire('key', 60000) // Expire in milliseconds
await redis.pexpireat('key', 1234567890000) // Expire at ms timestamp
await redis.persist('key') // Remove expiration
const ttl = await redis.ttl('key') // TTL in seconds
const pttl = await redis.pttl('key') // TTL in milliseconds
// Delete keys
await redis.del('key1', 'key2') // Synchronous delete
await redis.unlink('key1', 'key2') // Asynchronous delete
await redis.touch('key1', 'key2') // Update last access time
// Scan keys
const [cursor, keys] = await redis.scan(0, { match: 'user:*', count: 100 })Other Commands
await redis.ping() // 'PONG'
await redis.echo('hello') // 'hello'
await redis.dbsize() // Number of keys
await redis.flushdb() // Clear current database
await redis.flushall() // Clear all databasesPipeline
Batch multiple commands in a single round-trip:
const results = await redis
.pipeline()
.set('key1', 'value1')
.set('key2', 'value2')
.get('key1')
.get('key2')
.incr('counter')
.exec()
// results = ['OK', 'OK', 'value1', 'value2', 1]Transactions
Run commands atomically with MULTI:
const results = await redis
.multi()
.set('key', 'value')
.incr('counter')
.get('key')
.exec()
// All commands run atomicallyTypeScript Support
Full TypeScript support with generics:
interface User {
name: string
email: string
age: number
}
// Type-safe get
const user = await redis.get<User>('user:1')
if (user) {
console.log(user.name) // TypeScript knows this is a string
}
// Type-safe lists
const users = await redis.lrange<User>('users', 0, -1)
// Type-safe sets
await redis.sadd<User>('users', { name: 'Alice', email: '[email protected]', age: 30 })Connection Management
// Access underlying ioredis client if needed
const ioredisClient = redis.ioredis
// Close connection gracefully
await redis.quit()
// Force close connection
redis.disconnect()Compatibility
This library targets API compatibility with:
- @upstash/redis: 1.36.1
- ioredis: 5.9.2
Features Not Supported
These @upstash/redis features are HTTP-specific and not applicable:
readYourWrites/ sync tokens (TCP connections are inherently consistent)- HTTP retry logic (ioredis handles reconnection automatically)
- Telemetry headers
- Platform-specific builds (Cloudflare Workers, Fastly)
Additional Features
Thanks to ioredis, you get extra features not available in @upstash/redis:
- Persistent connections (lower latency)
- Connection pooling
- Pub/Sub support
- Streams with blocking operations
- Cluster support (via ioredis)
- Sentinel support (via ioredis)
License
MIT
Author
Javier Perez
- Website: javierperez.com
- Email: [email protected]
- GitHub: @appaka
Contributing
Issues and pull requests are welcome at github.com/appaka/redis.
