ADR-001: Dual API Pattern
Status
Accepted
Context
Midday SDK targets two distinct developer audiences:
- Application developers who want to quickly build dapps without learning new paradigms
- Advanced developers who want type-safe error handling, composability, and dependency injection
The underlying Midnight Network libraries are complex and require significant boilerplate. We needed an API design that:
- Lowers the barrier to entry for new developers
- Provides power features for production applications
- Avoids forcing developers to learn Effect.ts to use basic features
- Maintains type safety across all API styles
Decision
We provide three complementary API styles that share the same underlying implementation:
1. Promise API (Beginner-Friendly)
Simple async/await interface that feels familiar to JavaScript developers:
const client = await Midday.Client.create(config);const loaded = await client.loadContract({ module, zkConfig, privateStateId: 'my-id' });const deployed = await loaded.deploy();await deployed.actions.increment();2. Effect API (Functional Programming)
For developers who want Effect’s benefits without dependency injection:
const program = Effect.gen(function* () { const client = yield* Midday.Client.effect.create(config); const loaded = yield* client.effect.loadContract({ module, zkConfig, privateStateId: 'my-id' }); const deployed = yield* loaded.effect.deploy(); return yield* deployed.effect.actions.increment();});
await Midday.Runtime.runEffectPromise(program);3. Effect DI (Production Applications)
Full dependency injection for testability and modularity:
const program = Effect.gen(function* () { const client = yield* Midday.Client.MiddayClientService; const loaded = yield* client.effect.loadContract({ module, zkConfig, privateStateId: 'my-id' }); const deployed = yield* loaded.effect.deploy(); return yield* deployed.effect.actions.increment();});
await Effect.runPromise(program.pipe(Effect.provide(Midday.Client.layer(config))));Implementation Strategy
The core logic is written once using Effect.ts:
// Core implementation (Effect-based)function createEffect(config: ClientConfig): Effect.Effect<MiddayClient, ClientError> { return Effect.gen(function* () { // ... implementation });}
// Promise wrapperexport async function create(config: ClientConfig): Promise<MiddayClient> { return runEffectWithLogging(createEffect(config), config.logging ?? true);}
// Effect API exportexport const effect = { create: createEffect, // ...};
// DI Serviceexport class ClientService extends Context.Tag('ClientService')<ClientService, ClientServiceImpl>() {}export const ClientLive = Layer.succeed(ClientService, { create: createEffect, ... });Consequences
Benefits
- Low barrier to entry: Developers can start with Promise API immediately
- Gradual adoption: Teams can migrate to Effect API incrementally
- Single source of truth: All APIs share the same implementation
- No runtime overhead: Promise API adds minimal wrapper cost
- Type safety preserved: All three APIs maintain full TypeScript types
Tradeoffs
- API surface area: Three ways to do the same thing increases documentation needs
- Learning curve for Effect: Advanced features require Effect.ts knowledge
- Maintenance burden: Changes must be reflected in all API styles (mitigated by shared implementation)
Migration Path
Promise API → Effect API → Effect DI ↑ ↑ ↑ Quick start Composability TestabilityDevelopers can progressively adopt more powerful patterns as their needs grow.