Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik15 min czytania

Upstash

Upstash to serverless Redis, Kafka i QStash z pay-per-request pricing. Idealny dla Edge i Serverless z globalną dystrybucją i ultra-niskimi latencjami.

Upstash - Kompletny Przewodnik po Serverless Data Platform

Czym jest Upstash?

Upstash to serverless data platform oferująca Redis, Kafka i QStash z modelem pay-per-request. W przeciwieństwie do tradycyjnych rozwiązań, gdzie płacisz za działający serwer 24/7, w Upstash płacisz tylko za faktyczne użycie. To sprawia, że Upstash jest idealny dla aplikacji serverless i edge computing, gdzie zasoby są przydzielane dynamicznie.

Założony w 2020 roku, Upstash szybko zyskał popularność wśród deweloperów budujących aplikacje na Vercel, Cloudflare Workers, AWS Lambda i innych platformach serverless. Kluczową zaletą jest globalny edge network z replikami w 30+ lokalizacjach, zapewniający ultra-niskie latencje na całym świecie.

Upstash oferuje trzy główne produkty:

  • Upstash Redis - Serverless Redis z REST API
  • Upstash Kafka - Serverless Apache Kafka
  • QStash - HTTP-based message queue z scheduled delivery

Dlaczego Upstash?

Kluczowe zalety

  1. Pay-per-request - Płacisz tylko za użycie, zero kosztów przy braku ruchu
  2. Edge-native - Globalna dystrybucja z 30+ lokalizacjami
  3. REST API - Działa wszędzie, nawet w Edge Functions
  4. Zero maintenance - Brak zarządzania infrastrukturą
  5. Durable storage - Dane są replikowane i trwałe
  6. Generous free tier - 10K requests/day za darmo
  7. Native integrations - Vercel, Cloudflare, Fly.io

Upstash vs Redis Cloud vs Amazon ElastiCache

CechaUpstashRedis CloudElastiCache
Pricing modelPer-requestPer-hourPer-hour
Free tier10K/day30MBBrak
Edge locations30+10+AWS regions
REST API✅ Native❌ TCP only❌ TCP only
Serverless✅ TrueCzęściowo❌ Nie
Min. cost$0~$5/mo~$12/mo
Auto-scaling✅ AutomatycznyRęcznyRęczny
Edge compatible✅ Tak❌ Nie❌ Nie

Upstash Redis

Instalacja

Code
Bash
# SDK for JavaScript/TypeScript
npm install @upstash/redis

# SDK for Python
pip install upstash-redis

Konfiguracja

TSlib/redis.ts
TypeScript
// lib/redis.ts
import { Redis } from '@upstash/redis'

// Opcja 1: Z environment variables
export const redis = Redis.fromEnv()

// Opcja 2: Z explicit config
export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})

// Opcja 3: Dla Edge Runtime
export const redis = new Redis({
  url: 'https://xxx.upstash.io',
  token: 'AXxxxx',
  automaticDeserialization: true, // Automatyczny JSON parse
})
.env.local
ENV
# .env.local
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=AXxxxx

Podstawowe operacje

Code
TypeScript
import { redis } from '@/lib/redis'

// STRING operations
await redis.set('user:1:name', 'John Doe')
const name = await redis.get('user:1:name') // "John Doe"

// SET z TTL (expiration)
await redis.set('session:abc123', { userId: 1 }, { ex: 3600 }) // 1 hour

// SET z warunkami
await redis.set('lock:resource', 'locked', { nx: true }) // Tylko jeśli nie istnieje
await redis.set('config:version', '2.0', { xx: true }) // Tylko jeśli istnieje

// GET z typowaniem
interface User {
  id: number
  name: string
  email: string
}
const user = await redis.get<User>('user:1') // Typed!

// MGET/MSET - multiple keys
await redis.mset({
  'key1': 'value1',
  'key2': 'value2',
  'key3': 'value3',
})
const values = await redis.mget('key1', 'key2', 'key3')

// Increment/Decrement
await redis.incr('page:views')
await redis.incrby('user:1:points', 10)
await redis.decr('inventory:item:123')

// Append
await redis.append('log:today', '\n2024-01-15: User logged in')

// String length
const length = await redis.strlen('user:1:bio')

Hash operations

Code
TypeScript
// HSET - ustaw pola hash
await redis.hset('user:1', {
  name: 'John Doe',
  email: 'john@example.com',
  age: 30,
  verified: true,
})

// HGET - pobierz pojedyncze pole
const email = await redis.hget('user:1', 'email')

// HMGET - pobierz wiele pól
const [name, email] = await redis.hmget('user:1', 'name', 'email')

// HGETALL - pobierz cały hash
const user = await redis.hgetall<User>('user:1')

// HINCRBY - zwiększ wartość liczbową
await redis.hincrby('user:1', 'age', 1)

// HDEL - usuń pole
await redis.hdel('user:1', 'temporary_field')

// HEXISTS - sprawdź czy pole istnieje
const hasEmail = await redis.hexists('user:1', 'email')

// HKEYS/HVALS - klucze i wartości
const keys = await redis.hkeys('user:1')
const values = await redis.hvals('user:1')

List operations

Code
TypeScript
// LPUSH/RPUSH - dodaj elementy
await redis.lpush('queue:tasks', 'task1', 'task2')
await redis.rpush('queue:tasks', 'task3')

// LPOP/RPOP - pobierz i usuń element
const task = await redis.lpop('queue:tasks')
const lastTask = await redis.rpop('queue:tasks')

// LRANGE - pobierz zakres
const tasks = await redis.lrange('queue:tasks', 0, -1) // Wszystkie
const firstFive = await redis.lrange('queue:tasks', 0, 4)

// LLEN - długość listy
const queueLength = await redis.llen('queue:tasks')

// LINDEX - element pod indeksem
const secondTask = await redis.lindex('queue:tasks', 1)

// LSET - ustaw element pod indeksem
await redis.lset('queue:tasks', 0, 'updated_task')

// LTRIM - przytnij listę
await redis.ltrim('notifications', 0, 99) // Zachowaj ostatnie 100

Set operations

Code
TypeScript
// SADD - dodaj elementy
await redis.sadd('tags:post:1', 'javascript', 'react', 'typescript')

// SMEMBERS - wszystkie elementy
const tags = await redis.smembers('tags:post:1')

// SISMEMBER - czy element należy do zbioru
const hasTag = await redis.sismember('tags:post:1', 'react')

// SCARD - liczba elementów
const tagCount = await redis.scard('tags:post:1')

// SREM - usuń element
await redis.srem('tags:post:1', 'deprecated')

// SINTER - część wspólna zbiorów
const commonTags = await redis.sinter('tags:post:1', 'tags:post:2')

// SUNION - suma zbiorów
const allTags = await redis.sunion('tags:post:1', 'tags:post:2')

// SDIFF - różnica zbiorów
const uniqueTags = await redis.sdiff('tags:post:1', 'tags:post:2')

Sorted Set operations

Code
TypeScript
// ZADD - dodaj z score
await redis.zadd('leaderboard', {
  score: 100,
  member: 'player1',
})
await redis.zadd('leaderboard',
  { score: 150, member: 'player2' },
  { score: 80, member: 'player3' }
)

// ZRANGE - pobierz zakres (ascending)
const topPlayers = await redis.zrange('leaderboard', 0, 9, {
  rev: true, // Descending (highest first)
  withScores: true,
})

// ZSCORE - pobierz score członka
const score = await redis.zscore('leaderboard', 'player1')

// ZRANK - pozycja w rankingu
const rank = await redis.zrank('leaderboard', 'player1')
const rankFromTop = await redis.zrevrank('leaderboard', 'player1')

// ZINCRBY - zwiększ score
await redis.zincrby('leaderboard', 10, 'player1')

// ZRANGEBYSCORE - zakres po score
const playersAbove100 = await redis.zrangebyscore('leaderboard', 100, '+inf')

// ZREMRANGEBYRANK - usuń zakres
await redis.zremrangebyrank('leaderboard', 0, 9) // Usuń najsłabszych 10

JSON operations (RedisJSON)

Code
TypeScript
import { redis } from '@/lib/redis'

// Zapisz obiekt JSON
await redis.json.set('product:1', '$', {
  name: 'MacBook Pro',
  price: 2499,
  specs: {
    cpu: 'M3 Pro',
    ram: '18GB',
    storage: '512GB',
  },
  tags: ['laptop', 'apple', 'pro'],
})

// Pobierz cały obiekt
const product = await redis.json.get('product:1')

// Pobierz konkretną ścieżkę
const price = await redis.json.get('product:1', '$.price')
const cpu = await redis.json.get('product:1', '$.specs.cpu')

// Aktualizuj zagnieżdżone pole
await redis.json.set('product:1', '$.price', 2299)
await redis.json.set('product:1', '$.specs.ram', '36GB')

// Operacje na tablicach JSON
await redis.json.arrappend('product:1', '$.tags', 'new-tag')
await redis.json.arrlen('product:1', '$.tags')

// Numeryczne operacje
await redis.json.numincrby('product:1', '$.price', -100) // Discount

Pipelining i Transactions

Code
TypeScript
// Pipeline - wiele operacji w jednym request
const pipeline = redis.pipeline()

pipeline.set('key1', 'value1')
pipeline.set('key2', 'value2')
pipeline.incr('counter')
pipeline.hset('user:1', { lastActive: Date.now() })

const results = await pipeline.exec()
// results = [['OK', null], ['OK', null], [1, null], [1, null]]

// Multi/Exec - transakcja atomowa
const tx = redis.multi()

tx.decrby('account:A:balance', 100)
tx.incrby('account:B:balance', 100)

await tx.exec() // Atomowe przeniesienie środków

Rate Limiting

Upstash oferuje dedykowaną bibliotekę do rate limiting:

Code
Bash
npm install @upstash/ratelimit

Podstawowe rate limiting

Code
TypeScript
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

// Stwórz rate limiter
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
  analytics: true, // Włącz analytics dashboard
  prefix: '@upstash/ratelimit',
})

// Middleware dla Next.js
export async function middleware(request: NextRequest) {
  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'

  const { success, limit, remaining, reset } = await ratelimit.limit(ip)

  if (!success) {
    return new NextResponse('Too Many Requests', {
      status: 429,
      headers: {
        'X-RateLimit-Limit': limit.toString(),
        'X-RateLimit-Remaining': remaining.toString(),
        'X-RateLimit-Reset': reset.toString(),
        'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
      },
    })
  }

  return NextResponse.next()
}

Różne strategie rate limiting

Code
TypeScript
// Fixed Window - najprostszy
const fixedWindow = new Ratelimit({
  redis,
  limiter: Ratelimit.fixedWindow(100, '1 h'), // 100/hour
})

// Sliding Window - bardziej sprawiedliwy
const slidingWindow = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
})

// Token Bucket - burst-friendly
const tokenBucket = new Ratelimit({
  redis,
  limiter: Ratelimit.tokenBucket(10, '1 s', 30), // 10 tokens/sec, max 30
})

// Cached - wydajniejszy dla dużego ruchu
const cached = new Ratelimit({
  redis,
  limiter: Ratelimit.cachedFixedWindow(100, '10 s'),
  ephemeralCache: new Map(), // In-memory cache
})

Rate limiting per user/API key

Code
TypeScript
// Rate limit per authenticated user
export async function POST(request: Request) {
  const session = await getSession()
  const identifier = session?.user?.id ?? 'anonymous'

  const { success, remaining } = await ratelimit.limit(identifier)

  if (!success) {
    return Response.json(
      { error: 'Rate limit exceeded' },
      { status: 429 }
    )
  }

  // Dodaj info do response headers
  const response = await handleRequest(request)
  response.headers.set('X-RateLimit-Remaining', remaining.toString())

  return response
}

// Różne limity dla różnych tier-ów
const limits = {
  free: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(100, '1 d'),
  }),
  pro: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(10000, '1 d'),
  }),
  enterprise: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(1000000, '1 d'),
  }),
}

export async function POST(request: Request) {
  const user = await getUser()
  const limiter = limits[user.tier] || limits.free

  const { success } = await limiter.limit(user.id)
  // ...
}

QStash - Message Queue

QStash to serverless message queue z HTTP delivery, idealny dla background jobs i scheduled tasks.

Code
Bash
npm install @upstash/qstash

Podstawowe użycie

Code
TypeScript
import { Client } from '@upstash/qstash'

const qstash = new Client({
  token: process.env.QSTASH_TOKEN!,
})

// Wyślij wiadomość do endpoint
await qstash.publishJSON({
  url: 'https://my-app.com/api/process',
  body: {
    userId: '123',
    action: 'send-email',
    data: { template: 'welcome' },
  },
})

// Z opóźnieniem
await qstash.publishJSON({
  url: 'https://my-app.com/api/reminder',
  body: { userId: '123' },
  delay: '10m', // 10 minut
})

// Z retry
await qstash.publishJSON({
  url: 'https://my-app.com/api/webhook',
  body: { event: 'order.created' },
  retries: 5,
  callback: 'https://my-app.com/api/callback',
  failureCallback: 'https://my-app.com/api/failure',
})

Scheduled messages (Cron)

Code
TypeScript
// Jednorazowe scheduled message
await qstash.publishJSON({
  url: 'https://my-app.com/api/report',
  body: { type: 'weekly' },
  notBefore: Math.floor(Date.now() / 1000) + 86400, // Za 24h
})

// Recurring schedule (cron)
const schedule = await qstash.schedules.create({
  destination: 'https://my-app.com/api/daily-report',
  cron: '0 9 * * *', // Codziennie o 9:00 UTC
  body: JSON.stringify({ type: 'daily' }),
})

// Lista schedules
const schedules = await qstash.schedules.list()

// Usuń schedule
await qstash.schedules.delete(schedule.scheduleId)

Receiver - weryfikacja wiadomości

TSapp/api/process/route.ts
TypeScript
// app/api/process/route.ts
import { Receiver } from '@upstash/qstash'
import { NextResponse } from 'next/server'

const receiver = new Receiver({
  currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
  nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
})

export async function POST(request: Request) {
  const signature = request.headers.get('upstash-signature')!
  const body = await request.text()

  // Weryfikuj że wiadomość pochodzi z QStash
  const isValid = await receiver.verify({
    signature,
    body,
    url: process.env.VERCEL_URL + '/api/process',
  })

  if (!isValid) {
    return new NextResponse('Invalid signature', { status: 401 })
  }

  const data = JSON.parse(body)

  // Przetwórz wiadomość
  await processMessage(data)

  return new NextResponse('OK')
}

Batch operations

Code
TypeScript
// Wyślij wiele wiadomości naraz
const messages = users.map(user => ({
  url: 'https://my-app.com/api/notify',
  body: JSON.stringify({ userId: user.id }),
}))

await qstash.batchJSON(messages)

// Wyślij do wielu endpoint-ów
await qstash.publishJSON({
  url: [
    'https://my-app.com/api/analytics',
    'https://my-app.com/api/logging',
    'https://webhook.site/xxx',
  ],
  body: { event: 'user.signup', userId: '123' },
})

Topics (Pub/Sub)

Code
TypeScript
// Utwórz topic
await qstash.topics.create({ name: 'user-events' })

// Dodaj endpoint do topic
await qstash.topics.addEndpoint({
  topic: 'user-events',
  endpoint: 'https://service-a.com/webhook',
})
await qstash.topics.addEndpoint({
  topic: 'user-events',
  endpoint: 'https://service-b.com/webhook',
})

// Publikuj do topic - wszyscy subskrybenci otrzymają wiadomość
await qstash.publishJSON({
  topic: 'user-events',
  body: { event: 'user.created', userId: '123' },
})

Upstash Kafka

Serverless Apache Kafka z REST API.

Code
Bash
npm install @upstash/kafka

Producer

Code
TypeScript
import { Kafka } from '@upstash/kafka'

const kafka = new Kafka({
  url: process.env.UPSTASH_KAFKA_REST_URL!,
  username: process.env.UPSTASH_KAFKA_REST_USERNAME!,
  password: process.env.UPSTASH_KAFKA_REST_PASSWORD!,
})

const producer = kafka.producer()

// Wyślij pojedynczą wiadomość
await producer.produce('orders', {
  value: JSON.stringify({
    orderId: '123',
    items: ['item1', 'item2'],
    total: 99.99,
  }),
})

// Z kluczem partycji
await producer.produce('user-events', {
  key: 'user:123', // Wszystkie events tego usera w tej samej partycji
  value: JSON.stringify({
    event: 'page_view',
    page: '/products',
  }),
})

// Batch produce
await producer.produceMany([
  { topic: 'logs', value: 'Log entry 1' },
  { topic: 'logs', value: 'Log entry 2' },
  { topic: 'logs', value: 'Log entry 3' },
])

Consumer

Code
TypeScript
const consumer = kafka.consumer()

// Konsumuj wiadomości
const messages = await consumer.consume({
  consumerGroupId: 'my-group',
  instanceId: 'instance-1',
  topics: ['orders'],
  autoOffsetReset: 'earliest',
})

for (const message of messages) {
  console.log('Topic:', message.topic)
  console.log('Partition:', message.partition)
  console.log('Offset:', message.offset)
  console.log('Value:', message.value)

  // Przetwórz wiadomość
  await processOrder(JSON.parse(message.value))
}

// Commit offsets
await consumer.commit({
  consumerGroupId: 'my-group',
  instanceId: 'instance-1',
  topics: [
    {
      topic: 'orders',
      partitions: [
        { partition: 0, offset: messages[messages.length - 1].offset },
      ],
    },
  ],
})

Integracje

Next.js Edge Functions

TSapp/api/cache/route.ts
TypeScript
// app/api/cache/route.ts
import { Redis } from '@upstash/redis'

export const runtime = 'edge' // Działa na Edge

const redis = Redis.fromEnv()

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const key = searchParams.get('key')

  if (!key) {
    return Response.json({ error: 'Key required' }, { status: 400 })
  }

  const cached = await redis.get(key)

  if (cached) {
    return Response.json({ data: cached, source: 'cache' })
  }

  // Fetch fresh data
  const data = await fetchFreshData(key)
  await redis.set(key, data, { ex: 3600 })

  return Response.json({ data, source: 'fresh' })
}

Vercel KV (powered by Upstash)

Code
TypeScript
// Vercel KV jest oparty na Upstash Redis
import { kv } from '@vercel/kv'

// Identyczne API
await kv.set('key', 'value')
const value = await kv.get('key')
await kv.hset('hash', { field: 'value' })

Cloudflare Workers

TSworker.ts
TypeScript
// worker.ts
import { Redis } from '@upstash/redis/cloudflare'

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const redis = new Redis({
      url: env.UPSTASH_REDIS_REST_URL,
      token: env.UPSTASH_REDIS_REST_TOKEN,
    })

    const visits = await redis.incr('page:visits')

    return new Response(`This page has been visited ${visits} times`)
  },
}

Session Management

TSlib/session.ts
TypeScript
// lib/session.ts
import { Redis } from '@upstash/redis'
import { cookies } from 'next/headers'
import { nanoid } from 'nanoid'

const redis = Redis.fromEnv()

interface Session {
  userId: string
  email: string
  createdAt: number
}

export async function createSession(userId: string, email: string) {
  const sessionId = nanoid(32)
  const session: Session = {
    userId,
    email,
    createdAt: Date.now(),
  }

  await redis.set(`session:${sessionId}`, session, {
    ex: 60 * 60 * 24 * 7, // 7 days
  })

  const cookieStore = await cookies()
  cookieStore.set('session', sessionId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7,
  })

  return session
}

export async function getSession(): Promise<Session | null> {
  const cookieStore = await cookies()
  const sessionId = cookieStore.get('session')?.value

  if (!sessionId) return null

  const session = await redis.get<Session>(`session:${sessionId}`)

  if (!session) return null

  // Refresh TTL on access
  await redis.expire(`session:${sessionId}`, 60 * 60 * 24 * 7)

  return session
}

export async function deleteSession() {
  const cookieStore = await cookies()
  const sessionId = cookieStore.get('session')?.value

  if (sessionId) {
    await redis.del(`session:${sessionId}`)
    cookieStore.delete('session')
  }
}

Caching with SWR pattern

TSlib/cache.ts
TypeScript
// lib/cache.ts
import { Redis } from '@upstash/redis'

const redis = Redis.fromEnv()

interface CacheOptions {
  staleTime?: number // Czas po którym dane są "stale"
  maxAge?: number    // Maksymalny czas życia
}

export async function cachedFetch<T>(
  key: string,
  fetcher: () => Promise<T>,
  options: CacheOptions = {}
): Promise<{ data: T; stale: boolean }> {
  const { staleTime = 60, maxAge = 3600 } = options

  // Sprawdź cache
  const cached = await redis.get<{
    data: T
    timestamp: number
  }>(key)

  const now = Date.now()

  if (cached) {
    const age = (now - cached.timestamp) / 1000

    if (age < staleTime) {
      // Fresh - zwróć z cache
      return { data: cached.data, stale: false }
    }

    if (age < maxAge) {
      // Stale - zwróć z cache, odśwież w tle
      refreshInBackground(key, fetcher, maxAge)
      return { data: cached.data, stale: true }
    }
  }

  // Brak cache lub expired - pobierz fresh
  const data = await fetcher()
  await redis.set(key, { data, timestamp: now }, { ex: maxAge })

  return { data, stale: false }
}

async function refreshInBackground<T>(
  key: string,
  fetcher: () => Promise<T>,
  maxAge: number
) {
  try {
    const data = await fetcher()
    await redis.set(key, { data, timestamp: Date.now() }, { ex: maxAge })
  } catch (error) {
    console.error('Background refresh failed:', error)
  }
}

Cennik

Upstash Redis

PlanCenaRequestsBandwidthStorage
Free$0/mo10K/day50KB/req256MB
Pay as you go$0.2/100KUnlimited1MB/req10GB
Pro 2K$180/mo2M/day5MB/req50GB
EnterpriseCustomCustomCustomCustom

Upstash Kafka

PlanCenaMessagesPartitions
Free$0/mo10K/day3
Pay as you go$0.6/100KUnlimited50
EnterpriseCustomCustomUnlimited

QStash

PlanCenaMessagesSchedules
Free$0/mo500/day1
Pay as you go$1/100KUnlimitedUnlimited
Pro$40/mo500K/moUnlimited

FAQ - Najczęściej Zadawane Pytania

Czy Upstash Redis jest w pełni kompatybilny z Redis?

Tak, Upstash obsługuje większość komend Redis. Główna różnica to REST API zamiast TCP, co jest zaletą dla Edge/Serverless. Niektóre komendy blokujące (BLPOP, BRPOP) nie są wspierane ze względu na HTTP.

Jak działa global edge?

Upstash replikuje dane do 30+ lokalizacji na świecie. Request jest kierowany do najbliższej repliki. Write-y idą do primary i są propagowane do replik. Read latency to typowo <10ms.

Czy dane są trwałe?

Tak, Upstash używa durable storage. Dane są replikowane i backupowane. W przeciwieństwie do klasycznego Redis w pamięci, dane przetrwają restart.

Kiedy używać QStash vs Redis?

  • QStash - Dla background jobs, webhooks, scheduled tasks. Ma wbudowane retry, delivery confirmation, scheduling.
  • Redis - Dla cache, sessions, rate limiting, real-time data. Synchroniczne operacje.

Czy Upstash działa z Cloudflare Workers?

Tak, to jeden z głównych use-case'ów. REST API działa w środowiskach bez TCP (Edge Functions, Workers).