A type-safe HTTP client for querying @targetd/server instances with full TypeScript inference.
| JS Runtime | Command |
|---|---|
| Node.js | npx jsr add @targetd/api @targetd/client |
| Bun | bunx jsr add @targetd/api @targetd/client |
| Deno | deno add jsr:@targetd/api jsr:@targetd/client |
@targetd/client provides a strongly-typed HTTP client that mirrors the
@targetd/api Data interface. It allows you to
query remote targeting servers while maintaining full type safety and IDE
autocomplete.
Key features:
- Type-safe queries: Share type definitions between client and server
- Same API as Data: Uses identical methods to
@targetd/apiData class - Automatic serialization: Handles query parameter encoding/decoding
- Network-transparent: Query remote data as if it were local
Create a DataSchema that both client and server will use:
// shared/schema.ts
import { DataSchema, targetIncludes } from '@targetd/api'
import { z } from 'zod'
export const schema = DataSchema.create()
.usePayload({
greeting: z.string(),
config: z.object({
enabled: z.boolean(),
}),
})
.useTargeting({
country: targetIncludes(z.string()),
})Use @targetd/server to create an HTTP endpoint:
// server.ts
import { Data } from '@targetd/api'
import { createServer } from '@targetd/server'
import { schema } from './shared/schema.ts'
const serverData = await Data.create(schema)
.addRules('greeting', [
{
targeting: { country: ['US'] },
payload: 'Hello!',
},
{
targeting: { country: ['ES'] },
payload: '¡Hola!',
},
{
payload: 'Hi!',
},
])
.addRules('config', [
{
payload: { enabled: true },
},
])
createServer(serverData).listen(3000)Use the Client class with a local Data built from the same schema:
// client.ts
import { Data } from '@targetd/api'
import { Client } from '@targetd/client'
import { schema } from './shared/schema.ts'
const data = await Data.create(schema)
const client = new Client('http://localhost:3000', data)
// Type-safe queries with full autocomplete
const greeting = await client.getPayload('greeting', { country: 'US' })
// Returns: 'Hello!'
const config = await client.getPayload('config')
// Returns: { enabled: true }The Client class provides the same query methods as the Data class:
Get the first matching payload for a given name:
const payload = await client.getPayload('greeting', { country: 'ES' })
// Returns: '¡Hola!'Get all matching payloads:
const allMatches = await client.getPayloads('greeting', { country: 'US' })
// Returns array of all matching payloadsGet payloads for all registered names at once:
const allPayloads = await client.getPayloadForEachName({ country: 'US' })
// Returns: { greeting: 'Hello!', config: { enabled: true } }Here's a full example showing custom targeting descriptors, multiple payloads, and type safety:
// device.ts - Shared targeting descriptor
import { createTargetingDescriptor } from '@targetd/api'
import { z } from 'zod'
export const Device = z.enum(['desktop', 'mobile'])
export const deviceTargeting = createTargetingDescriptor({
predicate: (query) => (target) => target.includes(query),
queryParser: Device,
targetingParser: z.array(Device),
})// schema.ts - Shared schema
import { DataSchema } from '@targetd/api'
import { z } from 'zod'
import { deviceTargeting } from './device.ts'
export const schema = DataSchema.create()
.usePayload({
content: z.string(),
maxItems: z.number(),
})
.useTargeting({
device: deviceTargeting,
})// server.ts - Server implementation
import { Data } from '@targetd/api'
import { createServer } from '@targetd/server'
import { schema } from './schema.ts'
const serverData = await Data.create(schema)
.addRules('content', [
{
targeting: { device: ['mobile'] },
payload: '📱 Mobile Content',
},
{
targeting: { device: ['desktop'] },
payload: '🖥 Desktop Content',
},
])
.addRules('maxItems', [
{
targeting: { device: ['mobile'] },
payload: 10,
},
{
targeting: { device: ['desktop'] },
payload: 50,
},
])
createServer(serverData).listen(3000)// client.ts - Client usage
import { Data } from '@targetd/api'
import { Client } from '@targetd/client'
import { schema } from './schema.ts'
const data = await Data.create(schema)
const client = new Client('http://localhost:3000', data)
// Query for mobile
const mobileData = await client.getPayloadForEachName({ device: 'mobile' })
// Returns: { content: '📱 Mobile Content', maxItems: 10 }
// Query for desktop
const desktopData = await client.getPayloadForEachName({ device: 'desktop' })
// Returns: { content: '🖥 Desktop Content', maxItems: 50 }
// Type-safe: TypeScript knows 'device' must be 'mobile' or 'desktop'
// ✅ await client.getPayload('content', { device: 'mobile' })
// ❌ await client.getPayload('content', { device: 'tablet' }) // Type errorThe client inherits all type safety from the shared Data definition:
const client = new Client('http://localhost:3000', data)
// ✅ Type-safe payload names
await client.getPayload('content', { device: 'mobile' })
// ❌ Type error: 'unknown' is not a valid payload name
await client.getPayload('unknown')
// ❌ Type error: 'invalidParam' is not a valid query parameter
await client.getPayload('content', { invalidParam: 'value' })
// ✅ Return types are inferred correctly
const content: string = await client.getPayload('content', { device: 'mobile' })
const maxItems: number = await client.getPayload('maxItems', {
device: 'mobile',
})The client throws errors for network issues or server errors:
try {
const payload = await client.getPayload('content', { device: 'mobile' })
} catch (error) {
if (error instanceof Error) {
console.error('Request failed:', error.message)
}
}- @targetd/api - Core targeting and data querying API
- @targetd/server - HTTP server for serving targeted data
See LICENSE file for details.