Skip to content

Commit

Permalink
feat: docs updated why
Browse files Browse the repository at this point in the history
  • Loading branch information
rathod-sahaab committed Nov 14, 2024
1 parent e30ccbb commit fd21c31
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 15 deletions.
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ yarn add http-result

The core idea of `http-result` is to handle method responses as a **Result** type, which can either be a successful result with data or an error. This avoids unhandled errors and allows for more precise control over how errors are processed.

### Example: `createOrg`
### Example

Here’s an example using `http-result` to handle errors in a method that creates an organization:

**Internal Service code**

```typescript
import { HttpErrorResults, Ok, Result } from 'http-result'

Expand All @@ -70,20 +72,38 @@ class OrganisationService {
}): Promise<Result<Organisation, 'InternalServer' | 'NotFound'>> {
// Simulate error handling
if (!userId) {
return Err('BadRequest', 'Article too small')
// return HttpErrorResults.NotFound('User does not exists')
return Err('NotFound', 'User does not exist')
// You can also use
// return HttpErrorResults.NotFound('User does not exist')
}

if (name === 'Rick Astley') {
return HttpErrorResults.InternalServerError('Server got rick rolled')
}

if (!name) {
return HttpErrorResults.InternalServer('User does not exists')
// error can be BadRequest | NotFound
const [letter, letterError] = await this.letterService.sendLetter(name)

if (letterError) {
// repackage, preserves message from letterError in messages array
// send better more relevant errors to users even if generic code fails.
return HttpErrorResults.InternalServerError(
'Failed to send letter',
letterError,
)
}

// Simulate successful organization creation
const org = { id: '123', name }
Ok(org)

return Ok(org)
}
}
```

**API Gateway/Handler code**

```typescript
// caller function, API request handler in this case
import { tsRestError, TsRestResponse } from 'http-result/ts-rest'

Expand All @@ -95,11 +115,12 @@ const [org, error] = await organisationService.createOrg({
})

if (error.kind === 'NotFound') {
// special handling
// special handling, repackage the error to hid original
return TsRestResponse.BadRequest('You made a mistake!')
}

if (error) {
// forwar
return tsRestError(error) // Return a structured error response
}

Expand Down
116 changes: 116 additions & 0 deletions docs/why-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Why API?

This doc explains why API is the way it is, so when things change we can update the API for good and not take it as God's word.

## Result as array of `[data, error]`

Requirements for this API were:

1. Errors should not be ignore-able, i.e. must be handled explicitly.
2. Low mental bandwidth requirements, should fade away in the background.
3. Errors must have http status code.

Rust uses [enums](https://doc.rust-lang.org/rust-by-example/custom_types/enum.html) to implement result type, and you have to unpack that handle error and then proceed with your code.

```rust
// Result<f64, String>
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
```

Go on the other hand is skip-able on a type level, linters might catch this but code's good.

```go
result, err := divide(10, 2)
fmt.Println("Result:", result)
```

So the rust way is superior in this aspect. But, Typescript lacks **pattern matching** feature that would elegantly enable this form so here comes the solutions.

### Approaches

#### Tagged Unions/Discriminated Unions

```typescript
type Result<T, S extends HttpStatus> =
| { error: false; data: T }
| {
error: true
kind: S // 'BadRequest' | 'NotFound' | 'InternalServerError'
message: string
}
```
Usage:
```typescript
const result = await someServiceFunction(arg)

if (result.error) {
// handle error
}

// handle success

console.log(result.data)
```

But developer now has to worry about this result type and, it doesn't fade away
in the background need to write boilerplate code. Violate requirement (2).

#### Tuples

Tuples are like arrays but of fixed lengths

```typescript
type Result<T, S extends HttpStatus> = [T, null] | [null | ErrPayload<S>]
```
This gives us our current API
```typescript
const [data, error] = await someServiceFunction(name)

if (error) {
// handle error
}

console.log(data)
```

As soon as we de-structure the `data, error` everything is normal code.

### Helper Functions

Both of the above are easy to accept as a return value but hell to return from a function, for that we have some helper functions. Just like in `rust`

```typescript
return Ok(data)

return Err('BadRequest', 'This is a message')
return HttpErrorResults.BadRequest('This is a message')
```

There are two error returning helper functions. Depending on your editor/IDE
one might have better experience than other.

### Formatter

The core `Error` can then be converted into different frameworks' response
format using other set of helper functions.
They are separate from core code as you'd generally be using only one of them in your app.

- [x] `http-result/ts-rest`
- [ ] `http-result/express`
- [ ] `http-result/koa`

```typescript
import { tsRestError, TsRestResponse } from 'http-result/ts-rest'

return TsRestResponse.Created(org) // Return a successful response

return TsRestResponse.BadRequest('You made a mistake!')
return tsRestError(error) // Return a structured error response, automatically
```
2 changes: 1 addition & 1 deletion examples/live/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const router = s.router(contract, {
return TsRestResponse.OK(post)
},
createPost: async ({ body }) => {
const [post, createPostError] = serviceCreatePost(body.content)
const [post, createPostError] = await serviceCreatePost(body.content)

// if (!createPostError) {
if (post) {
Expand Down
16 changes: 14 additions & 2 deletions examples/live/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import {
IHttpErrorKind,
} from '../../src/index'
import { Post } from './contract'
import { createIndex } from './sub-service'

export function serviceCreatePost(
export async function serviceCreatePost(
article: string,
): Result<Post, 'InternalServerError' | 'BadRequest'> {
): Promise<Result<Post, 'InternalServerError' | 'BadRequest'>> {
if (article.length < 10) {
return Err('BadRequest', 'Article too small')
}
Expand All @@ -18,6 +19,17 @@ export function serviceCreatePost(
return HttpErrorResults.InternalServerError('Error storing in DB')
}

const [index, indexError] = await createIndex(article)

if (indexError) {
return HttpErrorResults.InternalServerError(
'Index Creation failed',
indexError,
)
}

console.log(index)

return Ok({ article })
}

Expand Down
12 changes: 12 additions & 0 deletions examples/live/sub-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Result, HttpErrorResults, Ok } from '../../src/index'

// if expecting, expect true, need help what to do here
export async function createIndex(
_article: string,
): Promise<Result<true, 'URITooLong'>> {
if (Math.random() > 0.5) {
HttpErrorResults.URITooLong('Too long url for article')
}

return Ok(true)
}
2 changes: 1 addition & 1 deletion examples/package/ts-rest/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const router = s.router(contract, {
return TsRestResponse.OK(post)
},
createPost: async ({ body }) => {
const [post, createPostError] = serviceCreatePost(body.content)
const [post, createPostError] = await serviceCreatePost(body.content)

// if (!createPostError) {
if (post) {
Expand Down
19 changes: 15 additions & 4 deletions examples/package/ts-rest/service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HttpErrorResults, Ok, Result } from 'http-result'
import { Err } from '../../../dist/index'
import { HttpErrorResults, Ok, Result, Err } from 'http-result'
import { Post } from './contract'
import { createIndex } from './sub-service'

export function serviceCreatePost(
export async function serviceCreatePost(
article: string,
): Result<Post, 'InternalServerError' | 'BadRequest'> {
): Promise<Result<Post, 'InternalServerError' | 'BadRequest'>> {
if (article.length < 10) {
return Err('BadRequest', 'Article too small')
}
Expand All @@ -13,5 +13,16 @@ export function serviceCreatePost(
return HttpErrorResults.InternalServerError('Error storing in DB')
}

const [index, indexError] = await createIndex(article)

if (indexError) {
return HttpErrorResults.InternalServerError(
'Index Creation failed',
indexError,
)
}

console.log(index)

return Ok({ article })
}
12 changes: 12 additions & 0 deletions examples/package/ts-rest/sub-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Result, HttpErrorResults, Ok } from 'http-result'

// if expecting, expect true, need help what to do here
export async function createIndex(
_article: string,
): Promise<Result<true, 'URITooLong'>> {
if (Math.random() > 0.5) {
HttpErrorResults.URITooLong('Too long url for article')
}

return Ok(true)
}

0 comments on commit fd21c31

Please sign in to comment.