Slide 1

Slide 1 text

Robust and Scalable API Gateway Built on Effect Yuichi Goto | June 2024 at TSKaigi 2024 After Talk

Slide 2

Slide 2 text

Who am I?  Yuichi Goto  @_yasaichi  @yasaichi  Senior SWE at EARTHBRAIN  Co-author of Ruby on Rails book 2 2

Slide 3

Slide 3 text

EB offers a novel hardware/software solution for the construction industry Image source: https://smartconstruction.com/ 3 3

Slide 4

Slide 4 text

Background and Purpose This talk is based on my experience in the recent migration project of a legacy API gateway that is now in production. The new API gateway is built on Deno, NestJS, and Effect. In this talk, I am going to Give an overview of Effect. Share the motivations, results, and key takeaways for adopting Effect in the project. 4 4

Slide 5

Slide 5 text

The original talk at TSKaigi in 5 mins (too short) https://speakerdeck.com/yasaichi/robust-and-scalable-api-gateway-built-on-effect 5 5

Slide 6

Slide 6 text

Agenda 1. Overview of Effect 2. Motivation and Results 3. Conclusion 6 6

Slide 7

Slide 7 text

What is Effect? Effect is a new TypeScript library that has just reached the stable release (v3) in April 2024 [1]. In theory, it provides the Effect System [2], one of the research topics in computer science [3]. In practice, it enables to build typed, robust, and scalable applications. It also provides standard-library like features such as Either type, Immutable Data Structures, Pattern Matching, etc. 7 7

Slide 8

Slide 8 text

See also: “Coroutines and effects” by @withoutboats https://without.boats/blog/coroutines-and-effects/ 8 8

Slide 9

Slide 9 text

Define and Create Effect s The type definition is Effect . From its type, you can think of it as a “smart Promise” that can explain both the success and error types. You can create effects From functions: Effect.sync , Effect.tryPromise , Effect.gen From the other values: Effect.succeed , Effect.fail 9 9

Slide 10

Slide 10 text

Compose and Run Effect s You can compose effects using pipe or yield* , which is similar to then and await in Promise. However, you must explicitly run effects using the following methods, which is different from Promise. Effect.run(Sync|Promise) returns a value or throws an error. Effect.run(Sync|Promise)Exit returns a value or an error. 10 10

Slide 11

Slide 11 text

import { Effect } from 'effect'; const remainder = (n: number, d: number): Effect.Effect => !isFinite(n) || !isFinite(d) || d === 0 ? Effect.fail(new Error('Cannot calculate the remainder')) : Effect.succeed(n % d); // NOTE: The error type is automatically inferred from `remainder`. const isEven = (n: number): Effect.Effect => remainder(n, 2).pipe(Effect.andThen((r) => r === 0)); Effect.runSync(isEven(42)); // true Effect.runSync(isEven(NaN)); // Error: Cannot calculate the remainder e.g. Remainder and isEven functions using Effect 11 11

Slide 12

Slide 12 text

Agenda 1. Overview of Effect 2. Motivation and Results 3. Conclusion 12 12

Slide 13

Slide 13 text

Motivation To solve a trade-off between the following things that result from I/O operations in the API gateway, I decided to use Effect. Robustness and Scalability The naive implementation is easy to maintain, but not very robust or scalable. Code maintainability Efforts to improve robustness and scalability result in less maintainability of the codebase. 13 13

Slide 14

Slide 14 text

export class UsersService { constructor( private readonly postApiService: PostApiService, private readonly userApiService: UserApiService, ) {} async findOne(id: number): Promise { try { const user = await this.userApiService.getUserById({ userId: id }); const posts = await this.postApiService.getPosts({ userId: id, limit: 5 }); return { id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), }; } catch (error) { if (error instanceof ApiException && error.code === 404) { throw new NotFoundException('User not found', { cause: error }); } throw error; } } } e.g. Retrieving a user and their latest posts * user and posts are just for explanation and don't appear in the real codebase. 14 14

Slide 15

Slide 15 text

// Before const user = await this.userApiService.getUserById({ userId: id }); // After const user = await retry( async () => { try { return await this.userApiService.getUserById({ userId: id }); } catch (error) { if (error instanceof ApiException && error.code === 404) { throw new AbortError( new NotFoundException('User not found', { cause: error }), ); } throw error; } }, { retries: 3 }, ); Improvement in robustness 15 15

Slide 16

Slide 16 text

// Before const user = await this.userApiService.getUserById({ userId: id }); const posts = await this.postApiService.getPosts({ userId: id, limit: 5 }); // After const [user, posts] = await Promise.all([ this.userApiService.getUserById({ userId: id }), this.postApiService.getPosts({ userId: id, limit: 5 }), ]); Improvement in scalability 16 16

Slide 17

Slide 17 text

async findOne(id: number): Promise { const [user, posts] = await Promise.all([ retry( async () => { try { return await this.userApiService.getUserById({ userId: id }); } catch (error) { if (error instanceof ApiException && error.code === 404) { throw new AbortError( new NotFoundException('User not found', { cause: error }), ); } throw error; } }, { retries: 3 }, ), retry( () => this.postApiService.getPosts({ userId: id, limit: 5 }), { retries: 3 }, ), ]); return { id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), }; } ... leads to less maintainability 17 17

Slide 18

Slide 18 text

Solution: 18 18

Slide 19

Slide 19 text

findOne(id: number): Effect.Effect { return Effect.all( [ Effect .tryPromise(() => this.userApiService.getUserById({ userId: id })) .pipe(Effect.retry({ until: (error) => error instanceof ApiException && error.code === 404, times: 3, })), Effect .tryPromise(() => this.postApiService.getPosts({ userId: id, limit: 5 })) .pipe(Effect.retry({ times: 3 })), ], { concurrency: 'inherit' }, ).pipe( Effect.catchAll(({ error }) => error instanceof ApiException && error.code === 404 ? Effect.fail(new NotFoundException('User not found', { cause: error })) : Effect.die(error) ), Effect.andThen(([user, posts]) => ({ id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), })), ); } Result: Well-organized multiple concerns 19 19

Slide 20

Slide 20 text

findOne(id: number): Effect.Effect { Effect .tryPromise(() => this.userApiService.getUserById({ userId: id })) Effect .tryPromise(() => this.postApiService.getPosts({ userId: id, limit: 5 })) Effect.andThen(([user, posts]) => ({ id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), })), } 1. API Requests and Response composition 20 20

Slide 21

Slide 21 text

findOne(id: number): Effect.Effect { .pipe(Effect.retry({ until: (error) => error instanceof ApiException && error.code === 404, times: 3, })), .pipe(Effect.retry({ times: 3 })), Effect.catchAll(({ error }) => error instanceof ApiException && error.code === 404 ? Effect.fail(new NotFoundException('User not found', { cause: error })) : Effect.die(error) } 2. Retrying API requests and Error handling 21 21

Slide 22

Slide 22 text

findOne(id: number): Effect.Effect { return Effect.all( [ ], { concurrency: 'inherit' }, ).pipe( ); } 3. Parallelizing API requests 22 22

Slide 23

Slide 23 text

Key Takeaways I ended up stopping using Effect in the whole codebase and decided to use it only in certain parts (e.g. Guard in NestJS). This was because the project members were giving negative feedback: “Difficult to read and write”. I did not expect that introducing the piping style would be as difficult as understanding the Effect concept. The Effect contributors understand these difficulties so well that they have done a lot of work around them. 23 23

Slide 24

Slide 24 text

The significant improvement in v3.0.4 [4] Source: https://x.com/MichaelArnaldi/status/1783129636915835255 24 24

Slide 25

Slide 25 text

// async/await const add = async () => { const x = await Promise.resolve(1); const y = await Promise.resolve(2); return x + y; }; // function*/yield* const add = Effect.gen(function* () { const x = yield* Effect.succeed(1); const y = yield* Effect.succeed(2); return x + y; }); Generators bring the same mental model as async/await 25 25

Slide 26

Slide 26 text

findOne(id: number): Effect.Effect { return Effect.gen(this, function* () { const [user, posts] = yield* Effect.all( [ Effect.retry( Effect.tryPromise(() => this.userApiService.getUserById({ userId: id })), { until: (error) => error instanceof ApiException && error.code === 404, times: 3, }, ), Effect.retry( Effect.tryPromise(() => this.postApiService.getPosts({ userId: id, limit: 5 })), { times: 3 }, ), ], { concurrency: 'inherit' }, ); return { id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), }; }).pipe( Effect.catchAll(({ error }) => error instanceof ApiException && error.code === 404 ? Effect.fail(new NotFoundException('User not found', { cause: error })) : Effect.die(error) ), ); } Future work: Effect with Generators in the whole codebase 26 26

Slide 27

Slide 27 text

Agenda 1. Overview of Effect 2. Motivation and Results 3. Conclusion 27 27

Slide 28

Slide 28 text

Conclusion Effect is a new library that provides a foundation for building a typed, robust, and scalable application in TypeScript. I adopted it to solve the trade-off between “robustness and scalability” and “code maintainability” in the recent project. I ended up using it only in the certain parts because introducing the piping style was also difficult for the project members. I am sure that Effect will get better because the contributors have done a lot of work around this difficulty. 28 28

Slide 29

Slide 29 text

In short, “Effect is TypeScript that scales” Source: https://x.com/MichaelArnaldi/status/1661478108447535105 29 29

Slide 30

Slide 30 text

Thank you This presentation is created by Marp. Great thanks @yhatt! 30 30

Slide 31

Slide 31 text

References 1. Effect 3.0 – Effect Blog,URL: https://effect.website/blog/effect-3.0 2. effect/README.md at e9875da3732bc67bb62789f2850d34abe7eb873d · Effect-TS/effect,URL: htt ps://github.com/Effect-TS/effect/blob/e9875da3732bc67bb62789f2850d34abe7eb873d/README. md#effect 3. Nielson, F., & Nielson, H.R. (1999). Type and Effect Systems. Correct System Design. 4. Release [email protected] · Effect-TS/effect,URL: https://github.com/Effect-TS/effect/releases/tag/effe ct%403.0.4 31 31