Skip to content

Your First Service

The Quick Start showed the bare minimum. This guide builds a more realistic service: multiple contexts, middleware, YAML configuration, and service-to-service calls.

Project Structure

A typical Strata service looks like this:

my-service/
├── config/
│   ├── local.yml
│   └── production.yml
├── src/
│   ├── contexts/
│   │   ├── users.ts
│   │   └── health.ts
│   └── service.ts
├── package.json
└── tsconfig.json

Contexts live in their own files and export a Context instance. The service file wires everything together. Config files are loaded per environment.

Define the Contexts

Users Context

Create src/contexts/users.ts:

typescript
import { Context } from '@strata-js/strata';

// -----------------------------------------------------------------------------------------

const users = new Context('users');

// -----------------------------------------------------------------------------------------

// In-memory store for this example
const userStore = new Map<string, { id : string; name : string; email : string }>();

users.registerOperation('create', async (request) =>
{
    const { name, email } = request.payload;
    const id = crypto.randomUUID();
    const user = { id, name, email };
    userStore.set(id, user);

    return { user };
});

users.registerOperation('get', async (request) =>
{
    const { id } = request.payload;
    const user = userStore.get(id);

    if(!user)
    {
        throw new Error(`User ${ id } not found`);
    }

    return { user };
});

users.registerOperation('list', async () =>
{
    return { users: Array.from(userStore.values()) };
});

// -----------------------------------------------------------------------------------------

export default users;

// -----------------------------------------------------------------------------------------

Each operation is an async function that receives the full request object. You can read request.payload, request.metadata, and request.auth. Return a value and Strata wraps it in a response envelope automatically.

Health Context

Create src/contexts/health.ts:

typescript
import { Context } from '@strata-js/strata';

// -----------------------------------------------------------------------------------------

const health = new Context('health');

// -----------------------------------------------------------------------------------------

health.registerOperation('check', async () =>
{
    return {
        status: 'ok',
        timestamp: new Date().toISOString(),
        uptime: process.uptime(),
    };
});

// -----------------------------------------------------------------------------------------

export default health;

// -----------------------------------------------------------------------------------------

Add Configuration

Install @strata-js/util-config for YAML config loading:

bash
npm install @strata-js/util-config

Create config/local.yml:

yaml
# Logging
logging:
  level: debug
  prettyPrint: true

# Service
service:
  serviceGroup: "MyService.$HOSTNAME"

# Aliases (friendly names for service groups)
aliases:
  myService: "MyService.$HOSTNAME"

# Backend
backend:
  type: "redis-streams"
  redis:
    host: "localhost"
    port: 6379

Environment variables are automatically substituted -- $HOSTNAME becomes the machine's hostname at load time. This means each developer gets their own isolated service group when running locally.

See Configuration for the full reference on every config option.

Wire Up the Service

Create src/service.ts:

typescript
import { hostname } from 'node:os';
import { StrataService, StrataConfig, logging } from '@strata-js/strata';
import configUtil from '@strata-js/util-config';

// Contexts
import usersContext from './contexts/users.js';
import healthContext from './contexts/health.js';

// -----------------------------------------------------------------------------------------

// Determine environment
const env = process.env.ENVIRONMENT ?? 'local';
process.env.HOSTNAME = process.env.HOSTNAME ?? hostname();

// Load config
configUtil.load(`./config/${ env }.yml`);

// Set up logging
logging.setConfig(configUtil.get<StrataConfig>()?.logging ?? {});

const logger = logging.getLogger('service');

// Create the service
const service = new StrataService(configUtil.get());

// Register contexts
service.registerContext(usersContext);
service.registerContext(healthContext);

// Start
async function main() : Promise<void>
{
    await service.start();
    logger.info('Service is ready.');
}

main().catch((error) =>
{
    logger.error('Failed to start service:', error.stack);
    process.exit(1);
});

// -----------------------------------------------------------------------------------------

A few things to note:

  • configUtil.load() reads the YAML file and performs environment variable substitution.
  • configUtil.get() returns the parsed config object. Since StrataService expects a StrataServiceConfig, and the YAML has the right shape, it just works.
  • Each context was created with a name (new Context('users'), new Context('health')), so registerContext can be called with just the context instance. You can also call service.registerContext('users', usersContext) if you prefer to name them at registration time.

Add Middleware

Middleware wraps operations with beforeRequest, success, and failure hooks. You can add middleware globally (on the service), per-context, or per-operation.

Here is a simple timing middleware that logs how long each request takes:

typescript
import type { OperationMiddleware } from '@strata-js/strata';
import { logging } from '@strata-js/strata';

const logger = logging.getLogger('timing');

const timingMiddleware : OperationMiddleware = {
    beforeRequest(request)
    {
        request['_startTime'] = Date.now();
        return request;
    },
    success(request)
    {
        const elapsed = Date.now() - (request['_startTime'] ?? 0);
        logger.info(`${ request.context }.${ request.operation } succeeded in ${ elapsed }ms`);
        return request;
    },
    failure(request)
    {
        const elapsed = Date.now() - (request['_startTime'] ?? 0);
        logger.warn(`${ request.context }.${ request.operation } failed in ${ elapsed }ms`);
        return request;
    },
};

export default timingMiddleware;

Register it globally in your service file:

typescript
service.useMiddleware(timingMiddleware);

Or register it on a specific context:

typescript
usersContext.useMiddleware(timingMiddleware);

Or on a single operation:

typescript
users.registerOperation('get', async (request) =>
{
    // ...handler code
}, [], [ timingMiddleware ]);

The third argument to registerOperation is opFuncParams (an array of request property paths to extract as function arguments). The fourth is middleware.

Service-to-Service Calls

If your service needs to call other services, create a StrataClient inside the same process. The client uses the same backend configuration but maintains its own response queue.

typescript
import { StrataClient, StrataClientConfig } from '@strata-js/strata';
import configUtil from '@strata-js/util-config';

// -----------------------------------------------------------------------------------------

let client : StrataClient | undefined;

export async function getClient() : Promise<StrataClient>
{
    if(!client)
    {
        const config = configUtil.get<StrataClientConfig>();
        client = new StrataClient({
            ...config,
            client: { name: 'MyServiceClient' },
        });

        await client.start();
    }

    return client;
}

// -----------------------------------------------------------------------------------------

Then use it inside an operation:

typescript
users.registerOperation('getWithOrders', async (request) =>
{
    const { userId } = request.payload;
    const user = userStore.get(userId);

    if(!user)
    {
        throw new Error(`User ${ userId } not found`);
    }

    // Call the OrderService to get orders for this user
    const client = await getClient();
    const orderResponse = await client.request('OrderService', 'orders', 'listByUser', {
        userId,
    });

    return { user, orders: orderResponse.payload.orders };
});

The client.request() call sends a message to the OrderService service group, targeting the orders context and listByUser operation. The response comes back as a ResponseEnvelope -- the payload is in response.payload.

Aliases

If you define aliases in your config, you can use the alias name instead of the raw service group:

yaml
aliases:
  orders: "OrderService.production"
typescript
await client.request('orders', 'orders', 'listByUser', { userId });

Run It

bash
# Make sure Redis is running
npx tsx src/service.ts

You should see log output showing the service starting up and listening on its service group.

To test it, create a quick client script or use the built-in service context that every Strata service gets for free:

typescript
import { StrataClient } from '@strata-js/strata';

const client = new StrataClient({
    client: { name: 'TestClient' },
    backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 } },
});

await client.start();

// Every service has a built-in 'service' context with an 'info' operation
const info = await client.request('MyService.your-hostname', 'service', 'info', {});
console.log(info.payload);

// Create a user
const created = await client.request('MyService.your-hostname', 'users', 'create', {
    name: 'Jane Doe',
    email: 'jane@example.com',
});
console.log('Created:', created.payload);

// List users
const listed = await client.request('MyService.your-hostname', 'users', 'list', {});
console.log('Users:', listed.payload);

process.exit(0);

Examples

The Strata project maintains a reference example service on GitLab: example-service. It demonstrates the same patterns covered here -- config loading, context registration, middleware, and client usage -- in a real project structure.

Next Steps