Thanks for contributing to Lily SDK. This repository is intended to be approachable for first-time contributors while still maintaining production-quality standards.
- Node.js 20 or newer
- npm 11 or newer
- Git
git clone https://github.com/lily-protocol/lily-sdk.git
cd lily-sdk
npm ci
npm run lint
npm run typecheck
npm run test- Create a focused branch from
main. - Make the smallest coherent change that solves one problem well.
- Add or update tests whenever behavior changes.
- Run
npm run lint,npm run typecheck, andnpm run testbefore opening a PR. - Update docs or examples when the public developer experience changes.
- Keep the public API ergonomic and strongly typed.
- Prefer small, composable modules over deep abstraction stacks.
- Avoid coupling SDK internals too tightly to backend implementation details unless the API contract is stable.
- Leave clear extension points for future contributors.
- TypeScript strict mode is required.
- ESLint and Prettier define the default style.
- Public types should be explicit and stable.
- New transport or client features should come with tests.
Clients follow a contract-driven pattern that keeps the public API explicit and centralizes transport behavior. Use these steps when adding a new endpoint group:
- Define the contract. Add a
*ClientContractinterface tosrc/types/contracts.ts. This file is the source of truth for each client's public operations: method names, inputs, and return types belong here, independent of the HTTP implementation. - Add the models. Define request, response, and query types in the appropriate module under
src/models/. Export new model types fromsrc/models/index.tsso contracts, consumers, and client implementations share the same definitions. - Implement the client. Add the implementation under
src/clients/, extendBaseClient, and implement its*ClientContract. Send endpoint requests through the inheritedBaseClient.requestmethod so all clients use the sharedHttpClientabstraction instead of duplicating transport logic. - Expose and compose it. Export the client from
src/index.ts, then add it to theLilySdkcomposition root insrc/sdk.ts: declare the client property and construct it with the resolved sharedHttpClient. - Add tests. Cover the client's request method, path, payload or query parameters, and response behavior. Also update the SDK composition tests to verify the new client is available from
LilySdk.
- A
*ClientContractis defined insrc/types/contracts.ts. - Request, response, and query models are added under
src/models/and exported fromsrc/models/index.ts. - The client extends
BaseClient, implements its contract, and usesBaseClient.request. - The client is exported from
src/index.tsand registered onLilySdkinsrc/sdk.ts. - Client behavior and SDK composition tests are added and all checks pass.
- Endpoint and schema alignment with Lily backend services
- Better retry policies and observability hooks
- Additional payment and wallet lifecycle methods
- Improved examples and integration recipes
- Release automation and npm publishing hardening
Lily SDK uses a contract-driven architecture to ensure consistency across all domain clients. Follow this five-step pattern when adding new functionality:
Add a new interface to src/types/contracts.ts that extends or mirrors existing client contracts:
export interface NewFeatureClientContract {
list(params?: ListParams): Promise<ListResponse>;
create(data: CreateRequest): Promise<CreateResponse>;
}Create request/response types in src/models/new-feature.ts and export them from src/models/index.ts:
export interface CreateRequest { /* ... */ }
export interface CreateResponse { /* ... */ }Create src/clients/new-feature-client.ts extending BaseClient:
import { BaseClient } from './base-client';
import type { NewFeatureClientContract } from '../types/contracts';
export class NewFeatureClient extends BaseClient implements NewFeatureClientContract {
async list(params?: ListParams) {
return this.request({ method: 'GET', path: '/new-feature', query: params });
}
}Update src/sdk.ts to compose the new client and expose it as a public property:
this.newFeature = new NewFeatureClient(this.transport);Export relevant symbols from src/index.ts.
Write unit tests in tests/new-feature.test.ts covering happy paths, error cases, and contract compliance. Use stubbed fetch for deterministic results.
- Contract defined in
src/types/contracts.ts - Models added to
src/models/and re-exported - Client implements contract via
BaseClient - Registered in
LilySdkconstructor (src/sdk.ts) - Public exports updated in
src/index.ts - Unit tests cover success, error, and edge cases
- README or docs updated if user-facing behavior changes
- Keep PR descriptions clear and outcome-focused.
- Link related issues when possible.
- Call out breaking changes explicitly.
- Include follow-up work if you intentionally defer part of the implementation.
Lily SDK uses a contract-driven pattern to ensure consistency and testability across all API clients. When adding support for a new Lily service or endpoint group, follow this five-step checklist:
Add a new interface to src/types/contracts.ts that extends the logical grouping of operations. Contracts define the public shape of the client without tying it to HTTP details:
export interface ExampleClientContract {
list(query?: ListExamplesQuery): Promise<readonly Example[]>;
get(id: string): Promise<Example>;
create(input: CreateExampleRequest): Promise<Example>;
}Create request/response types in src/models/ and re-export them from src/models/index.ts. Keep models pure data structures; validation and transformation belong in the client or config layer.
Create src/clients/example-client.ts extending BaseClient. Use this.request() for all HTTP calls so retry, timeout, and transport logic stay centralized:
import { BaseClient } from './base-client';
import type { ExampleClientContract } from '../types/contracts';
export class ExampleClient extends BaseClient implements ExampleClientContract {
public async list(query?: ListExamplesQuery): Promise<readonly Example[]> {
return this.request({ method: 'GET', path: '/examples', query });
}
}Export the new client from src/index.ts and add it as a property in src/sdk.ts:
// src/sdk.ts
this.example = new ExampleClient(resolvedHttpClient);This ensures every LilySdk instance exposes the new client with the shared transport and configuration.
Write unit tests in tests/example-client.test.ts using the stubbed HttpClient pattern established by existing tests. Cover success paths, error propagation, and edge cases like empty responses or non-JSON payloads.
- Consistency: Every client follows the same lifecycle, making the SDK predictable.
- Testability: Contracts allow mocking at the interface level without HTTP coupling.
- Maintainability: Transport concerns (retries, timeouts, auth) live in
BaseClientandHttpClient, not duplicated per endpoint. - Discoverability: New contributors can trace from contract → model → client → SDK registration without guessing.
See src/types/contracts.ts, src/clients/base-client.ts, and src/sdk.ts for reference implementations.