@steam-relay/api-contract
v0.1.6
Published
Shared API contracts and typed client for steam-relay-server
Readme
@steam-relay/api-contract
Type-safe TypeScript client, schemas, and route definitions for the steam-relay-server API.
This package is the source of truth for endpoint request and response schemas, normalized problem-style error payloads, and a typed HTTP client for external consumers.
Features
- 🎯 Type-safe API contracts – Full TypeScript support with Zod schemas
- 📦 Modular exports – Import only what you need:
routes,schemas,client, ortypes - 📋 Runtime validation – Zod schemas validate responses
- 🔌 Dual ESM/CJS – CommonJS and ES Modules support
- 💾 Minimal footprint – Tree-shakeable exports
Quick Start
Using the pre-built client
import { createClient } from "@steam-relay/api-contract";
const client = createClient({
baseUrl: "https://your-steam-relay-server.example.com",
});
const response = await client.request("/api/v1/watchers", {
method: "GET",
});
if (!response.ok) {
console.error(`Error: ${response.statusCode} - ${response.message}`);
return;
}
const { data } = response;
console.log(data.watchers);Using schemas directly
import {
watcherSchema,
createWatcherBodySchema,
setWatcherItemsBodySchema,
apiWebhookPayloadSchema
} from "@steam-relay/api-contract";
const parsed = watcherSchema.parse(rawData);
const validated = createWatcherBodySchema.safeParse(req.body);
const itemsUpdate = setWatcherItemsBodySchema.safeParse(req.body);
const webhookData = apiWebhookPayloadSchema.safeParse(payload);Using route definitions
import { routes } from "@steam-relay/api-contract";
// Access route metadata
const watchersRoute = routes["/api/v1/watchers"];Exports
client
Pre-configured API client factory using @md-oss/api-types.
import { createClient, type Client } from "@steam-relay/api-contract";routes
Route registry with endpoint definitions.
import { routes, type Routes } from "@steam-relay/api-contract";schemas
Zod schemas for all API requests and responses.
types
TypeScript type definitions inferred from schemas.
Watcher Schemas
Create/Update Watcher
import { createWatcherBodySchema, type CreateWatcherBody } from "@steam-relay/api-contract";
const body: CreateWatcherBody = {
name: "My Workshop Watcher",
appId: "221100",
webhookUrl: "https://discord.com/api/webhooks/123/abc",
enabled: true,
watchApps: true,
watchWorkshop: true,
};
const validated = createWatcherBodySchema.parse(body);Bulk Set Watcher Items
Atomically replace all tracked workshop items for a watcher. If any item ID doesn't exist, the operation fails without changes.
import { setWatcherItemsBodySchema, type SetWatcherItemsBody } from "@steam-relay/api-contract";
const body: SetWatcherItemsBody = {
itemIds: ["1234567890", "1234567891", "1234567892"],
};
const validated = setWatcherItemsBodySchema.parse(body);
// Usage:
// POST /api/v1/watchers/:watcherId/items/set
// Response: 200 OK with updated watcher, or 404 if any items don't existWebhook Schemas
The webhook system supports two transport types detected automatically by URL pattern:
Discord Webhooks
Discord URLs receive rich embeds:
import { webhookTypeSchema, type WebhookType } from "@steam-relay/api-contract";
// Discord webhooks are auto-detected from URL pattern
// https://discord.com/api/webhooks/{id}/{token}
// https://discordapp.com/api/webhooks/{id}/{token}
// https://canary.discord.com/api/webhooks/{id}/{token}
// https://ptb.discord.com/api/webhooks/{id}/{token}
// Discord payload (internal):
{
username: "Steam Relay",
avatar_url: "https://...",
embeds: [
{
title: "App Updated",
description: "Arma 3 has been updated",
fields: [...]
}
]
}API Webhooks
Generic HTTP endpoints receive structured JSON with typed event payloads:
import {
apiWebhookPayloadSchema,
type ApiWebhookPayload,
type ApiWebhookEventType
} from "@steam-relay/api-contract";
// Validate incoming webhook payload
const payload = apiWebhookPayloadSchema.parse({
source: "steam-relay-server",
timestamp: "2026-05-02T14:30:00Z",
eventType: "app.updated",
data: {
appId: "107410",
appName: "Arma 3",
isFree: false,
shortDescription: "Tactical combat simulator",
steamAppId: 107410,
type: "DLC",
},
});API Event Types
Four predefined event types, each with a specific data schema:
app.updated
Single app update.
import { appUpdatedDataSchema, type AppUpdatedData } from "@steam-relay/api-contract";
const data: AppUpdatedData = {
appId: "107410",
appName: "Arma 3",
isFree: false,
shortDescription: "Tactical combat simulator",
steamAppId: 107410,
type: "DLC",
};app.updated.batch
Multiple app updates in a single webhook call.
import { appUpdatedBatchDataSchema, type AppUpdatedBatchData } from "@steam-relay/api-contract";
const data: AppUpdatedBatchData = {
count: 2,
items: [
{
appId: "107410",
appName: "Arma 3",
isFree: false,
shortDescription: "Tactical combat simulator",
steamAppId: 107410,
type: "DLC",
},
// ... more items
],
label: "Daily App Update Batch",
};steam.status
Steam API health status (failure or recovery event).
import { steamStatusDataSchema, type SteamStatusData } from "@steam-relay/api-contract";
const data: SteamStatusData = {
status: "degraded",
consecutiveFailures: 3,
lastFailureTime: "2026-05-02T14:25:00Z",
errorType: "429",
statusCode: 429,
url: "https://api.steampowered.com/...",
previousFailures: 2,
};workshop.items.batch
Batched workshop item updates (new, updated, or removed).
import { workshopItemsBatchDataSchema, type WorkshopItemsBatchData } from "@steam-relay/api-contract";
const data: WorkshopItemsBatchData = {
count: 2,
items: [
{
action: "updated",
consumerAppId: "221100",
itemId: "1234567890",
timeUpdated: 1746270645,
title: "Better Buildings",
},
{
action: "new",
consumerAppId: "221100",
itemId: "1234567891",
timeUpdated: 1746270700,
title: "Advanced Crafting",
},
],
};Complete Example: Webhook Consumer
import { apiWebhookPayloadSchema } from "@steam-relay/api-contract";
// Express route handler
app.post("/steam-updates", (req, res) => {
try {
const payload = apiWebhookPayloadSchema.parse(req.body);
switch (payload.eventType) {
case "app.updated":
console.log(`${payload.data.appName} updated`);
break;
case "app.updated.batch":
console.log(
`Batch update for ${payload.data.count} apps: ${payload.data.label}`
);
break;
case "steam.status":
console.log(
`Steam API status: ${payload.data.status} (${payload.data.consecutiveFailures} failures)`
);
break;
case "workshop.items.batch":
payload.data.items.forEach((item) => {
console.log(`[${item.action}] ${item.title} (${item.itemId})`);
});
break;
}
res.status(200).json({ received: true });
} catch (error) {
console.error("Invalid webhook payload", error);
res.status(400).json({ error: "Invalid payload" });
}
});