Skip to content

rstackio/services

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

34 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Services

Minimal TypeScript toolkit for async data fetching β€” typed errors, chainable transforms, zero-config mocking.

npm CI coverage License: MIT TypeScript


πŸ’‘ Philosophy

Most async data layers grow into a tangle of try/catch blocks, ad-hoc mocks, and one-off error handling scattered across components. Services is built to fix that at the source β€” the main goal is to make async data work feel smooth and predictable, so developers spend less time on plumbing and more time building features.

A few guiding principles:

  • Errors are values, not exceptions. [error, data] tuples make error handling explicit and type-safe β€” no surprises, no forgotten catch blocks.
  • Define once, run everywhere. Transforms, error handlers, and mocks are declared on the provider, not at every call site. Change behavior in one place.
  • Mocking should cost nothing. Tests and dev mode use real mock files β€” no framework magic, no vi.mock gymnastics. Toggle from the console, swap per test with .andMock().
  • No hidden abstractions. The provider is just a function. await getUser(id) β€” it does exactly what you'd expect.
  • Colocation over convention. Each feature owns its api, mock, normalize, and provider files. The data layer lives next to the feature that uses it.

✨ Features

  • πŸ›‘οΈ [error, data] tuples β€” createSafeProvider never throws; errors are typed values
  • πŸ”— Chainable API β€” .andMock() .andThen() .andCatch() .andFinally() compose once, apply on every call
  • πŸ§ͺ Zero-config testing β€” mocks activate automatically in NODE_ENV=test; swap them per test with .andMock()
  • 🎭 Dev mocking β€” toggle mocks from the browser console without touching source code
  • ⚑ Abort support β€” AbortSignal flows through providers and delay() helpers

πŸ“¦ Modules

Module Purpose Docs
data-provider Wrap async functions in a chainable, mockable, type-safe API API Β· Usage
mock Toggle mock mode, simulate latency, SSR-compatible state API Β· Usage
safe Wrap any function to return [error, data] instead of throwing API
logger Structured console logger, silent in production API Β· Usage

πŸ—‚οΈ File structure

This is a suggested folder structure β€” not enforced by the library. Use whatever layout fits your project. The pattern that works well in practice:

src/
└── modules/
    └── user/
        β”œβ”€β”€ models/
        β”‚   └── user.model.ts      ← types and validation schema, API and UI models
        └── data-provider/
            β”œβ”€β”€ api.ts             ← real async function
            β”œβ”€β”€ mock.ts            ← mock using delay()
            β”œβ”€β”€ normalize.ts       ← pure function to transform API response to UI model
            β”œβ”€β”€ provider.ts        ← wires api, mock and transforms together; also a natural place for React Query options
            └── index.ts           ← export * from './provider'

api.ts β€” fetch from real endpoint

import ky from 'ky';
import { userApiSchema } from '../models/user.model';

export const getUser = (id: string, signal?: AbortSignal) =>
  ky.get(`/api/users/${id}`, { signal }).json(userApiSchema);

ky can be extended to add runtime schema validation β€” keeping individual api.ts files clean while ensuring runtime types always match build-time types.

mock.ts β€” simulated response with realistic delay

import { delay } from '@rstackio/services/mock';

export const getUser = (id: string, signal?: AbortSignal) =>
  delay(() => ({ id, firstName: 'Jane', lastName: 'Doe', role: 'admin' as const }), { signal, delayMs: 300 });

normalize.ts β€” pure transform, easy to test in isolation

import type { UserApi, User } from '../models/user.model';

export const normalize = (user: UserApi): User => ({
  ...user,
  fullName: `${user.firstName} ${user.lastName}`,
});

provider.ts β€” wires everything together once

import { createSafeProvider } from '@rstackio/services/data-provider';
import * as api from './api';
import * as mock from './mock';
import { normalize } from './normalize';

export const getUser = createSafeProvider(api.getUser)
  .andMock(mock.getUser) // βœ… mock must match the exact signature of api.getUser
  .andThen(normalize);   // βœ… final type is inferred from normalize's return type β€” callers get User, not UserApi

Type safety β€” .andMock() enforces that the mock matches the original signature. .andThen() transforms the result type β€” the caller always sees the output of the last transform, fully inferred with no manual annotations needed.

index.ts β€” single public entry point

export * from './provider';

Consume anywhere in the module:

import * as Dp from './data-provider';

const [error, user] = await Dp.getUser(id);

πŸš€ Installation

npm install @rstackio/services
# or
pnpm add @rstackio/services
# or
yarn add @rstackio/services

Requires TypeScript 5.0+ with strict: true.


πŸ› οΈ Contributing

pnpm install
pnpm test          # run tests
pnpm test:watch    # watch mode
pnpm build         # build dist

See CONTRIBUTING.md for the full guide, including how to add a changeset to your PR.


πŸ‘₯ Contributors

Amazing people who made their contributions. Feel free to contribute!


License: MIT

About

TypeScript toolkit for async data fetching

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors