Client

Learn how to use Probitas clients to interact with various services and protocols. This guide covers available clients, common patterns, and best practices for effective testing.

Overview

Probitas provides unified client APIs for connecting to external services during scenario testing. All clients share common patterns:

  • Unified namespace: Access all clients via client.*
  • Automatic cleanup: Clients implement AsyncDisposable for resource management
  • Consistent options: Common settings like timeout, retry, and abort signal
  • Built-in assertions: Use expect() for response validation
import { client, expect, scenario } from "probitas";

export default scenario("API Test")
  .resource("http", () =>
    client.http.createHttpClient({
      url: "http://localhost:8080",
    }))
  .step("Make request", async (ctx) => {
    const { http } = ctx.resources;
    const res = await http.get("/health");
    expect(res).toBeSuccessful();
  })
  .build();

Common Options

All clients accept common options like timeout, signal, and retry. See Configuration for detailed retry settings.

Resource Lifecycle

Register clients as resources for automatic lifecycle management. Resources are disposed in reverse order after the scenario completes.

.resource("http", () =>
  client.http.createHttpClient({ url: "..." })
)
// Automatically disposed when scenario ends

For manual control outside scenarios, use await using:

await using http = client.http.createHttpClient({
  url: "http://localhost:8080",
});
// Automatically closed when scope exits

HTTP Client

The HTTP client provides a fluent API for making HTTP requests with built-in JSON handling and response assertions.

const http = client.http.createHttpClient({
  url: "http://localhost:8080",
  headers: { "Content-Type": "application/json" },
  throwOnError: true,
});

See Configuration for all options.

Making Requests

The client supports all standard HTTP methods:

// GET with query parameters
const res = await http.get("/users", {
  query: { page: 1, limit: 10 },
});

// POST with JSON body
const res = await http.post("/users", {
  name: "Alice",
  email: "alice@example.com",
});

// PUT, PATCH, DELETE
await http.put("/users/1", { name: "Alice Updated" });
await http.patch("/users/1", { email: "new@example.com" });
await http.delete("/users/1");

Override client settings per request:

// Custom headers for authenticated request
const res = await http.get("/protected", {
  headers: { Authorization: "Bearer token123" },
});

// Disable error throwing for expected failures
const res = await http.get("/maybe-404", {
  throwOnError: false,
});
if (!res.ok) {
  console.log("Status:", res.status);
}

Assertions

Validate responses with chainable assertions:

expect(res)
  .toBeSuccessful() // Status 2xx
  .toHaveStatus(200) // Exact status code
  .toHaveHeader("content-type", /application\/json/) // Content-Type pattern
  .toHaveContentContaining({ name: "Alice" }) // Partial JSON match
  .toHaveDurationLessThan(1000); // Response time limit

// Additional assertions
expect(res).not.toBeSuccessful(); // Status not 2xx
expect(res).toHaveHeader("X-Request-Id");
expect(res).toHaveText(/success/);

SQL Clients

Probitas supports multiple SQL databases with a consistent query interface.

Database Client Factory
PostgreSQL client.sql.postgres.createPostgresClient()
MySQL client.sql.mysql.createMySqlClient()
SQLite client.sql.sqlite.createSqliteClient()
DuckDB client.sql.duckdb.createDuckDbClient()
const pg = client.sql.postgres.createPostgresClient({
  url: {
    host: "localhost",
    port: 5432,
    database: "testdb",
    user: "testuser",
    password: "testpass",
  },
});

See Configuration for all options.

Queries

Run queries with parameterized values:

// Simple query
const result = await pg.query("SELECT * FROM users");

// Parameterized query (type-safe)
const result = await pg.query<{ id: number; name: string }>(
  "SELECT * FROM users WHERE id = $1",
  [userId],
);

// Access results
const users = result.rows.all(); // All rows
const first = result.rows.first(); // First row
const count = result.count; // Row count

Transactions

Wrap multiple queries in a transaction:

const result = await pg.transaction(async (tx) => {
  const insert = await tx.query<{ id: number }>(
    "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
    ["Alice", "alice@example.com"],
  );

  await tx.query(
    "INSERT INTO profiles (user_id, bio) VALUES ($1, $2)",
    [insert.rows.first().id, "Hello!"],
  );

  return insert.rows.first();
});

Assertions

Validate query results:

expect(result)
  .toBeSuccessful()
  .toHaveRowCount(1)
  .toHaveContentContaining({ name: "Alice" });

// Match multiple rows
expect(result).toHaveContent([
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
]);

gRPC Client

The gRPC client supports unary calls, server streaming, client streaming, and bidirectional streaming.

const grpc = client.grpc.createGrpcClient({
  url: "localhost:50051",
  metadata: { authorization: "Bearer token" },
});

See Configuration for all options.

Unary Calls

Standard request-response pattern:

const res = await grpc.call("echo.EchoService", "Echo", {
  message: "Hello",
});

expect(res).toBeSuccessful().toHaveContentContaining({ message: "Hello" });
const data = res.data();

Server Streaming

Receive multiple responses from a single request:

const messages: unknown[] = [];
for await (
  const res of grpc.serverStream("echo.EchoService", "ServerStream", {
    count: 3,
  })
) {
  expect(res).toBeSuccessful();
  messages.push(res.data());
}

Client Streaming

Send multiple requests, receive a single response:

const res = await grpc.clientStream(
  "echo.EchoService",
  "ClientStream",
  async function* () {
    yield { message: "First" };
    yield { message: "Second" };
    yield { message: "Third" };
  },
);
expect(res).toBeSuccessful();

Bidirectional Streaming

Stream in both directions simultaneously:

for await (
  const res of grpc.bidiStream(
    "echo.EchoService",
    "BidiStream",
    async function* () {
      yield { message: "Ping 1" };
      yield { message: "Ping 2" };
    },
  )
) {
  expect(res).toBeSuccessful();
  console.log("Received:", res.data());
}

ConnectRPC Client

The ConnectRPC client supports Connect, gRPC, and gRPC-Web protocols with a unified API.

const connect = client.connectrpc.createConnectRpcClient({
  url: "localhost:8080",
});

Unary Calls

const res = await connect.call("echo.EchoService", "Echo", {
  message: "Hello",
});

expect(res).toBeSuccessful().toHaveContentContaining({ message: "Hello" });

Server Streaming

for await (
  const res of connect.serverStream("echo.EchoService", "ServerStream", {
    count: 3,
  })
) {
  expect(res).toBeSuccessful();
  console.log("Received:", res.data());
}

GraphQL Client

The GraphQL client provides methods for queries, mutations, and subscriptions.

const graphql = client.graphql.createGraphqlClient({
  url: "http://localhost:4000/graphql",
  headers: { Authorization: "Bearer token" },
  wsUrl: "ws://localhost:4000/graphql",
});

See Configuration for all options.

Queries

Fetch data with GraphQL queries:

import { outdent } from "probitas";

const res = await graphql.query(
  outdent`
    query GetUser($id: ID!) {
      user(id: $id) {
        id
        name
        email
      }
    }
  `,
  { id: "1" },
);

expect(res).toBeSuccessful().toHaveContentContaining({
  user: { name: "Alice" },
});
const user = res.data().user;

Mutations

Modify data with GraphQL mutations:

const res = await graphql.mutate(
  outdent`
    mutation CreateUser($input: CreateUserInput!) {
      createUser(input: $input) {
        id
        name
      }
    }
  `,
  { input: { name: "Alice", email: "alice@example.com" } },
);

expect(res).toBeSuccessful();
const newUser = res.data().createUser;

Subscriptions

Listen for real-time updates:

const subscription = graphql.subscribe(outdent`
  subscription OnUserCreated {
    userCreated {
      id
      name
    }
  }
`);

for await (const res of subscription) {
  expect(res).toBeSuccessful();
  console.log("New user:", res.data().userCreated);
}

Redis Client

The Redis client provides operations for strings, hashes, lists, and sets.

const redis = client.redis.createRedisClient({
  url: "redis://localhost:6379",
});

See Configuration for all options.

Operations

Common Redis operations:

// Strings
await redis.set("key", "value");
await redis.set("key", "value", { ex: 3600 }); // With TTL
const result = await redis.get("key");
expect(result).toBeSuccessful().toHaveContent("value");

// Hashes
await redis.hset("user:1", { name: "Alice", age: "30" });
const user = await redis.hgetall("user:1");

// Lists
await redis.lpush("queue", "task1", "task2");
const task = await redis.rpop("queue");

// Sets
await redis.sadd("tags", "typescript", "deno");
const tags = await redis.smembers("tags");

// Delete
await redis.del("key");

MongoDB Client

The MongoDB client provides document operations with a familiar API.

const mongo = client.mongodb.createMongoClient({
  uri: "mongodb://localhost:27017",
  database: "testdb",
});

See Configuration for all options.

Operations

Work with collections and documents:

const users = mongo.collection<User>("users");

// Insert
const result = await users.insertOne({
  name: "Alice",
  email: "alice@example.com",
});
expect(result).toBeSuccessful();

// Find
const user = await users.findOne({ _id: result.insertedId });
expect(user).toBeSuccessful().toHaveContentContaining({ name: "Alice" });

// Find many
const allUsers = await users.find({ age: { $gte: 18 } }).toArray();

// Update
await users.updateOne(
  { _id: result.insertedId },
  { $set: { name: "Bob" } },
);

// Delete
await users.deleteOne({ _id: result.insertedId });

Deno KV Client

The Deno KV client provides access to Deno's built-in key-value store.

const kv = client.deno_kv.createDenoKvClient();

By default, an in-memory database is used for testing.

Operations

// Set and get
await kv.set(["users", "1"], { name: "Alice" });
const result = await kv.get(["users", "1"]);
expect(result).toBeSuccessful().toHaveContent({ name: "Alice" });

// List by prefix
const users = await kv.list({ prefix: ["users"] });
for await (const entry of users) {
  console.log(entry.key, entry.value);
}

// Atomic operations
const atomic = kv.atomic();
atomic
  .check({ key: ["users", "1"], versionstamp: null })
  .set(["users", "1"], { name: "Alice" });
const commitResult = await atomic.commit();

// Delete
await kv.delete(["users", "1"]);

RabbitMQ Client

The RabbitMQ client provides AMQP messaging for publish/subscribe patterns.

const rabbitmq = client.rabbitmq.createRabbitMqClient({
  url: "amqp://guest:guest@localhost:5672",
});

See Configuration for all options.

Operations

const channel = await rabbitmq.channel();

// Declare queue
await channel.assertQueue("my-queue", { durable: false });

// Send message
const content = new TextEncoder().encode(JSON.stringify({ message: "Hello" }));
await channel.sendToQueue("my-queue", content);

// Receive message
const result = await channel.get("my-queue");
expect(result).toBeSuccessful().toHaveContent();
if (result.message) {
  await channel.ack(result.message);
}

await channel.close();

SQS Client

The AWS SQS client provides cloud message queue operations.

const sqs = client.sqs.createSqsClient({
  endpoint: "http://localhost:4566", // LocalStack or AWS endpoint
  region: "us-east-1",
  credentials: {
    accessKeyId: "...",
    secretAccessKey: "...",
  },
});

See Configuration for all options.

Operations

// Ensure queue exists
await sqs.ensureQueue("my-queue");

// Send message
const result = await sqs.send(JSON.stringify({ event: "user.created" }));
expect(result).toBeSuccessful().toHaveMessageId();

// Receive and process
const messages = await sqs.receive({ maxMessages: 10 });
for (const msg of messages) {
  console.log("Received:", msg.body);
  await msg.delete();
}

Available Clients

Client Factory Function Use Case
HTTP client.http.createHttpClient() REST APIs, webhooks
PostgreSQL client.sql.postgres.createPostgresClient() PostgreSQL databases
MySQL client.sql.mysql.createMySqlClient() MySQL databases
SQLite client.sql.sqlite.createSqliteClient() Embedded databases
DuckDB client.sql.duckdb.createDuckDbClient() Analytics databases
gRPC client.grpc.createGrpcClient() gRPC services
ConnectRPC client.connectrpc.createConnectRpcClient() Connect/gRPC-Web
GraphQL client.graphql.createGraphqlClient() GraphQL APIs
Redis client.redis.createRedisClient() Cache, pub/sub
MongoDB client.mongodb.createMongoClient() Document databases
Deno KV client.deno_kv.createDenoKvClient() Deno KV store
RabbitMQ client.rabbitmq.createRabbitMqClient() AMQP message queues
SQS client.sqs.createSqsClient() AWS message queues

Best Practices

Register Clients as Resources

Always register clients as resources for automatic cleanup:

// Good - automatic lifecycle management
.resource("http", () => client.http.createHttpClient(...))

// Avoid - manual cleanup required
.step("Make request", async () => {
  const http = client.http.createHttpClient(...);
  // Must manually dispose
})

Use Type Parameters

Provide type parameters for type-safe responses:

// Good - typed response
const result = await pg.query<{ id: number; name: string }>(
  "SELECT id, name FROM users WHERE id = $1",
  [userId],
);
const user = result.rows.first(); // Type: { id: number; name: string }

// Avoid - untyped response
const result = await pg.query("SELECT * FROM users");

Handle Errors Appropriately

Use assertions for expected successes, explicit checks for expected failures:

// Expected success - use assertions
const res = await http.get("/users/1");
expect(res).toBeSuccessful().toHaveStatus(200);

// Expected failure - disable throwing, check manually
const res = await http.get("/users/nonexistent", { throwOnError: false });
expect(res).toHaveStatus(404);

Configure Retries

Use retry configuration for network-dependent operations. See Configuration for all retry options.

.step("External API call", async (ctx) => {
  const { http } = ctx.resources;
  const res = await http.get("/external-api", {
    retry: { maxAttempts: 3, backoff: "exponential" },
  });
  expect(res).toBeSuccessful();
}, {
  timeout: 10000, // Allow time for retries
})
Search Documentation