@aios-medical/cashier
v1.4.0
Published
Unified subscription billing for Node.js and TypeScript. One API across payment providers, with typed errors and NestJS support.
Downloads
1,859
Maintainers
Readme
Cashier
Unified subscription billing for Node.js and TypeScript. One API across payment providers, with typed errors and NestJS and Express support. Stripe and Recurly are supported today.
Cashier puts payment providers behind one set of methods and types. Your code asks for a driver, calls driver.subscriptions.create(...), and gets back the same Subscription shape whichever provider is behind it. Each provider is a driver, so new providers can be added without changing your code. Provider errors are turned into a small set of typed errors, so you can handle a declined card or a missing customer the same way everywhere.
- One interface for every provider, so you can switch providers or run several side by side.
- Consistent results: amounts in minor units (cents), uppercase ISO 4217 currency codes,
Dateobjects for timestamps. - Typed errors such as
NotFoundError,PaymentFailedErrorandRateLimitError, with the original provider error kept ascause. - Works from CommonJS and ES modules, with TypeScript types included.
- First-class NestJS (
CashierModule.forRoot, injectableCashierService) and Express (req.cashier, error handler) packages. - Switch providers with one config value, or use other credentials at runtime.
- No runtime dependencies besides the provider clients you install yourself.
Packages
| Package | Use it for |
| ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| @aios-medical/cashier | The core library. Works in any Node.js or TypeScript project. |
| @aios-medical/cashier-nestjs | NestJS module with forRoot, forRootAsync, CashierService and an exception filter. |
| @aios-medical/cashier-express | Express middleware for req.cashier and an error handler. |
All packages are released together with the same version.
Providers
| Provider | Driver name | SDK (peer dependency) |
| -------- | ------------------------- | ----------------------------------------------------------------- |
| Stripe | CashierProvider.Stripe | stripe v13 |
| Recurly | CashierProvider.Recurly | recurly v4.67 or later |
Install
npm install @aios-medical/cashier stripe recurlyEach driver uses its provider's official SDK, installed as a peer dependency. Install the SDKs of every provider in the table above, since Cashier currently loads all of them. Node.js 20 or later is required.
Quick start
import {
CashierProvider,
createCashier,
PaymentFailedError,
} from '@aios-medical/cashier';
const cashier = createCashier({
default: CashierProvider.Stripe,
providers: {
stripe: { apiKey: process.env.STRIPE_SECRET_KEY! },
recurly: { apiKey: process.env.RECURLY_API_KEY! },
},
});
const customer = await cashier.use().customers.create({
email: '[email protected]',
firstName: 'Jane',
lastName: 'Doe',
});
try {
const subscription = await cashier.use().subscriptions.create({
customer: customer.id,
price: 'price_123',
currency: 'USD',
});
console.log(subscription.status, subscription.items[0]?.unitAmount);
} catch (error) {
if (error instanceof PaymentFailedError) {
console.log('Payment failed', error.declineCode);
} else {
throw error;
}
}use picks the provider for each call:
| Call | Driver |
| ------------------------------------------------- | ---------------------------------------------------------------- |
| cashier.use() | The default provider. Switch providers by changing the config. |
| cashier.use(CashierProvider.Recurly) | A provider from providers. |
| cashier.use(CashierProvider.Stripe, { apiKey }) | Other credentials, given at runtime. |
Drivers are created once and reused, so calling use on every request is cheap. See TypeScript.
Use with
TypeScript
The core package is all you need. Pass the Cashier to your classes so tests can replace it. See TypeScript.
NestJS
npm install @aios-medical/cashier-nestjs@Module({
imports: [
CashierModule.forRootAsync({
isGlobal: true,
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
default: CashierProvider.Stripe,
providers: {
stripe: { apiKey: config.getOrThrow('STRIPE_SECRET_KEY') },
},
}),
}),
],
providers: [{ provide: APP_FILTER, useClass: CashierExceptionFilter }],
})
export class AppModule {}
@Injectable()
export class BillingService {
constructor(private readonly cashier: CashierService) {}
getInvoice(invoiceId: string) {
return this.cashier.use().invoices.get(invoiceId);
}
}See NestJS.
Express
npm install @aios-medical/cashier-expressapp.use(cashierMiddleware(cashier));
app.get('/invoices/:id', async (req, res) => {
res.json(await req.cashier.use().invoices.get(req.params.id));
});
app.use(cashierErrorHandler());See Express.
Documentation
Every resource works the same way on each provider. The full reference, with parameters, provider behavior and returned objects, is in docs/.
| Resource | Methods |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Customers | get, list, create, update |
| Invoices | get, list, cursorPaginate, pay, void |
| Payments | list, cursorPaginate, refund |
| Products | get, list |
| Prices | get, list |
| Subscriptions | list, cursorPaginate, create, get, update, cancel |
Guides:
- Drivers and conventions: amounts, currencies, metadata, Recurly codes and pagination.
- Errors: every error class, how provider errors are mapped, and the HTTP responses.
- TypeScript, NestJS and Express: setup and testing for each.
Errors
Every method rejects with a subclass of CashierError, such as NotFoundError, PaymentFailedError or RateLimitError. Each error has a code, the provider, the provider's HTTP status and error code, and the original error as cause. See Errors for the full list.
import { NotFoundError } from '@aios-medical/cashier';
try {
await cashier.use().customers.get('cus_123');
} catch (error) {
if (error instanceof NotFoundError) return null;
throw error;
}Contributing
See CONTRIBUTING.md. To report a security issue, see SECURITY.md.
License
MIT © AIOS
