Composable micro services in TypeScript for micro apps: patterns and pitfalls
microservicesarchitecturebest-practices

Composable micro services in TypeScript for micro apps: patterns and pitfalls

UUnknown
2026-02-20
11 min read
Advertisement

Practical patterns to split micro apps into tiny TypeScript services with typed contracts, avoiding type drift and deployment sprawl.

Stop losing time to broken contracts: composable TypeScript services for micro apps

You built a tiny "Where2Eat" micro app that needs auth, recommendations, and analytics — fast. But when you split those responsibilities into separate TypeScript services you hit familiar walls: inconsistent types, duplicated validation, brittle deploys, and noisy observability. This guide shows how to create tiny, composable TypeScript services for micro apps while avoiding the common pitfalls teams face in 2026.

Why this matters in 2026

The last 18 months accelerated two trends that change how we design micro apps. First, the rise of "micro apps" and vibe-coding — non-developers and rapidly iterating teams shipping single-purpose apps — has increased demand for small, reusable backend building blocks. Second, adoption of high-performance analytics platforms (for example, ClickHouse's continued growth in late 2025 and early 2026) and serverless edge runtimes means analytics and recommendation engines often live as separate services requiring typed contracts and reliable telemetry.

High-level pattern: tiny composable services with typed contracts

At its core, the pattern is simple:

  1. Define a single source of truth for data shapes and APIs (the contract).
  2. Share that contract across consumer and provider packages (compile-time types + runtime validation).
  3. Use a small client factory to compose services at runtime with guarantees (typed RPC, typed events, typed telemetry).
  4. Automate compatibility checks in CI and keep deployment surface area small where it matters.

Design goals

  • Composability: tiny services should be easily assembled into larger features.
  • Type safety: consumers and providers share the same types, preventing integration bugs.
  • Lightweight deploys: avoid a combinatorial explosion of deployments for tiny changes.
  • Observability: typed telemetry that maps to OLAP-ready schemas (ClickHouse, BigQuery).

Pattern in code: a minimal contract-first approach

The most robust way to keep types consistent is to treat your contract as the source of truth and generate both runtime validators and TypeScript types from it. Zod is a common choice in 2026 for this pattern because of its ergonomics and wide adoption, but you can use json-schema, TypeBox, or OpenAPI depending on your toolchain.

Example: a shared contract package

Create a lightweight workspace package, e.g., packages/contracts, that exports both runtime validators and TypeScript types. This package is published or referenced inside your monorepo.

// packages/contracts/src/index.ts
import { z } from 'zod';

export const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
});

export type User = z.infer;

export const RecommendationRequest = z.object({
  userId: z.string().uuid(),
  context: z.object({ location: z.string().optional(), prompt: z.string().optional() }).optional(),
});

export const RecommendationResponse = z.object({
  items: z.array(z.object({ id: z.string(), score: z.number() })),
});

export type RecommendationRequestT = z.infer;
export type RecommendationResponseT = z.infer;

The provider (recommendations service) uses the same module to validate incoming payloads; the consumer uses the same types to get compile-time guarantees.

Typed HTTP client factory

A compact client factory takes schema validators and returns typed request helpers. This avoids duplicating fetch code across multiple micro apps.

// packages/clients/src/factory.ts
import type { ZodType } from 'zod';

export function createTypedClient(
  baseUrl: string,
  path: string,
  reqSchema?: Req,
  resSchema?: Res,
) {
  return async (body: Req extends ZodType ? z.infer : unknown) => {
    const raw = await fetch(`${baseUrl}${path}`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(body),
    });
    const json = await raw.json();
    if (resSchema) resSchema.parse(json);
    return json as Res extends ZodType ? z.infer : unknown;
  };
}

Use it from the front-end or from another micro service:

// example usage
import { createTypedClient } from 'clients';
import { RecommendationRequest, RecommendationResponse } from 'contracts';

const getRecommendations = createTypedClient(
  'https://recommendations.internal',
  '/v1/suggest',
  RecommendationRequest,
  RecommendationResponse,
);

// Type-safe at call site:
const res = await getRecommendations({ userId: '...' });
res.items[0].score; // typed number

Architecture patterns: where to split and where to bundle

Micro services should be small, but not so tiny that deployments and operational overhead explode. Use these pragmatic rules:

  • Split by lifecycle and ownership: group things that change together (auth flows + session store) and separate cross-cutting concerns with stable contracts.
  • Bundle by latency sensitivity: keep super-low-latency paths local (in-process or edge functions) and put heavier async jobs (model-based recommendations) behind service boundaries.
  • Expose composable APIs: small RPC endpoints + event topics that can be composed at runtime by orchestrators or frontends.

Example service groupings

  • Auth service: token issuance, session validation, SSO integrations — accessible via typed RPC and middleware.
  • Recommendations service: model scoring, caching, feature retrieval — async-friendly and versioned.
  • Analytics service: ingest typed events, buffer to OLAP warehouse (e.g. ClickHouse), and expose aggregated queries.

Key pitfalls and how to avoid them

You will face recurring problems. Below are practical patterns and tooling to avoid them.

Pitfall: inconsistent types between services

Symptom: compile-time types diverge, production payloads break validators.

Fixes
  • Create a contracts package with runtime validators and types (Zod/json-schema/OpenAPI).
  • Publish contracts as a versioned artifact (npm package or monorepo workspace) with strict semver for breaking changes.
  • Add a CI job that installs provider & consumer together and runs integration tests that parse/validate requests and responses.

Pitfall: contract drift due to ad-hoc patches

Symptom: multiple services silently accept different shapes; migrations fail.

Fixes
  • Use contract evolution rules: additive fields are fine; removals require a major version bump.
  • Keep compatibility tests in CI that run older clients against newer providers to validate backward compatibility.
  • For event schemas, adopt a schema registry and enforce compatibility (both forward and backward where necessary).

Pitfall: deployment sprawl and operational cost

Symptom: dozens of tiny services each need deployment pipelines, infra, and observability — the overhead outweighs the benefit.

Fixes
  • Group micro services into logical deploy units based on release cadence and ownership. Not everything needs its own CI/CD pipeline.
  • Use platform features like function-as-a-service or process managers to host multiple small services inside one deployment when they share lifecycle.
  • Adopt infrastructure automation (monorepo-aware CI like Turborepo, NX, or pnpm workspaces) to reduce per-service boilerplate.

Pitfall: observability gaps for composed systems

Symptom: hard to trace a user's journey across auth -> recommendations -> analytics.

Fixes
  • Propagate a typed correlation ID in your contracts. Make it part of the contract so every service can log and surface the same ID.
  • Emit typed telemetry with agreed schemas. Map telemetry to OLAP-friendly rows and push into a columnar store (ClickHouse, BigQuery) for fast analysis; this trend accelerated in 2025–2026.
  • Instrument RPC calls with latency and error metrics; tie them back to typed events for richer debugging.
"Treat telemetry and contracts with the same engineering rigor — they're both first-class inputs to debugging and product insight." — senior platform engineer

Advanced TypeScript techniques to keep services small and safe

We'll use a few TypeScript patterns that scale well for micro services: generics for client factories, discriminated unions for events, and conditional types for feature flags.

1) Generic typed clients

The client factory earlier used generics to enforce request and response shapes. Expand that to a typed RPC dispatcher supporting multiple endpoints.

// rpc/client.ts
type Endpoint = {
  path: string;
  req: Req;
  res: Res;
};

export function createRpcClient>>(base: string) {
  return async (key: K, body: Spec[K] /* typed at call sites */) => {
    const path = (body as any).path || (key as string);
    const res = await fetch(base + path, { method: 'POST', body: JSON.stringify(body) });
    return res.json() as Promise ? R : unknown>;
  };
}

2) Discriminated unions for event types

Use a discriminant field (e.g., eventType) so analytics service can pattern-match and your TypeScript compiler helps you exhaustively handle events.

// packages/contracts/events.ts
import { z } from 'zod';

export const ClickEvent = z.object({ eventType: z.literal('click'), elementId: z.string(), time: z.number() });
export const PurchaseEvent = z.object({ eventType: z.literal('purchase'), orderId: z.string(), amount: z.number() });

export const AppEvent = z.discriminatedUnion('eventType', [ClickEvent, PurchaseEvent]);
export type AppEventT = z.infer;

3) Feature-flagged types with conditional types

When you have optional features across micro apps (A/B tests, region-specific fields), use conditional types to express feature presence in compile-time contracts — combined with runtime checks in contracts package.

Operational patterns: CI, versioning, and compatibility

The best architecture fails without automation. Add these to your pipelines.

  • Contract CI: On every PR, run: type-check, run shared contract validators, and run integration tests where a mock consumer calls a local provider. Fail on validator mismatch.
  • Compatibility Matrix: Maintain a lightweight compatibility matrix for major client/provider versions used by micro apps. Test all supported combinations during release windows.
  • Canary & Feature Flags: Deploy new contract versions behind flags. Gradually enable and monitor typed telemetry to detect runtime issues before full rollout.

Observability: typed telemetry and analytics readiness

Analytics microservices benefit from columnar stores. In 2026, ClickHouse and other OLAP engines have become mainstream for product analytics pipelines — they excel at ingesting typed event streams from micro apps. To make events useful:

  • Define event schemas in the contracts package and validate at ingress.
  • Normalize events in a lightweight buffer service and write to your OLAP sink in row format suited for the engine (e.g., MergeTree for ClickHouse).
  • Use typed telemetry for quick queries; include correlation IDs and user context shapes so analysts can slice by meaningful attributes.

Case study: building a tiny recommendations service for micro apps

A product team built a recommendations service that serves 30 distinct micro apps. Early problems: each app kept its own ad-hoc request shape and cache logic; deployment was per-app. After adopting a contracts-first approach they unified payloads into a single contracts package, introduced a typed client, and grouped release cycles into two deploy units: the recommendations engine and the adapter layer.

Outcomes within 3 months:

  • 40% fewer production errors caused by payload mismatch.
  • Reduced number of deploy pipelines from 30 to 4 (grouped adapters by region), saving operational effort.
  • Faster incident triage because every event had a typed correlation ID and schema.

Checklist: ship composable TypeScript micro services

  1. Create a contracts package with runtime validators (Zod/json-schema/OpenAPI).
  2. Use a typed client factory and share it across micro apps.
  3. Group services into deploy units by lifecycle and latency needs.
  4. Automate contract validation in CI and test compatibility regularly.
  5. Version contracts with strict semver and publish changes with migration notes.
  6. Emit typed telemetry, propagate correlation IDs, and pipeline events into an OLAP store for analytics.
  7. Use schema registries for events if you have asynchronous pipelines.

Expect these patterns and investments to matter more through 2026:

  • AI-assisted contract generation: tools that create initial Zod/OpenAPI schemas from user stories will speed up initial micro app wiring, but always vet runtime guarantees manually.
  • Edge-first composability: tiny services deployed to the edge will push latency-sensitive logic closer to users while keeping heavy models centralized.
  • Event-driven observability: teams increasingly use OLAP stores (ClickHouse and others) as their primary analytics backend; invest early in typed ingestion pipelines.

When not to use tiny services

Micro services are not a silver bullet. Consider bundling when:

  • Change rate is synchronous across features — if you always change auth + recommendation together, keep them in one unit.
  • Operational cost of separate deploys is higher than the benefits of separation.
  • Team size or expertise is insufficient to maintain robust CI and observability for many services.

Actionable takeaways

  • Start with contracts: extract Zod/OpenAPI schemas early and share them, not ad-hoc types.
  • Automate compatibility: CI should fail on contract drift, not later in production.
  • Group deploys thoughtfully: prefer logical deploy units over blindly one-service-per-function.
  • Invest in typed telemetry: your analytics microservices should write directly into OLAP with typed rows for fast insights.

Conclusion & call-to-action

Splitting micro apps into tiny, composable TypeScript services unlocks speed and reuse — but only if you treat contracts, observability, and deployments as first-class concerns. In 2026, the most resilient teams standardize contracts (runtime + compile-time), automate compatibility checks, and back typed telemetry into OLAP systems so micro apps remain small, composable, and debuggable.

Ready to try it? Extract one contract from an existing micro app this week, add a Zod validator and a typed client, and run an integration test that validates the end-to-end flow. Share the result with your team and iterate on versioning rules.

If you want a checklist or a starter monorepo with contracts, typed clients, and CI examples tailored to your stack, drop your platform and runtime (Node.js, Deno, or edge) and I’ll provide a minimal starter template.

Advertisement

Related Topics

#microservices#architecture#best-practices
U

Unknown

Contributor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

Advertisement
2026-02-20T02:10:40.583Z