(fp-ts series 1: Container)
- Option
- Either
- TaskEither (part 1) — Basic introduction
- TaskEither (part 2) — Dependent Promises
- TaskEither (part 3) — Independent Promises
- 👇🏻 TaskEither (part 4) — More examples
Quick Review on TaskEither
In the previous article TaskEither (part 1), you can gain more understanding on the basic of how to use TaskEither to wrap a Promise value , and examples are also provided to illustrate the concepts of TaskEither.
In TaskEither (part 2), we focus on solving Dependent Promises using Do Notation in TaskEither.
In TaskEither (part 3), we focus on solving Independent Promises using Applicative and Monadic ways in TaskEither.
In this article, more examples are provided.
Example — creating an endpoint on Express.js
Suppose we have an findOrder
endpoint on our backend NodeJS server using Express.js. This endpoint aims to support finding orders from an order service. To do so, we first need to fetch a token from an auth service.
Next, we receive an array of Order
from the order service, which contains an array of ProductId
in each Order
object. Then, we map the array of ProductId
into an array of Product
by asking the product service.
Finally, we return the an array of Order
, which contains an array of Product
in each Order
object.
Let's dive into the codes!
Fetching auth token
Let's create a function to fetch the auth token.
/**
* Function that fetch the authToken
*/
const fetchAuthToken = taskEither.tryCatchK(
// Mock the token
({authUrl}) => Promise.resolve('DEMO_AUTH_TOKEN'),
reason =>
new Error(`Failed to fetch authToken due to Error ${String(reason)}`)
);
For demo purpose, we will just return a dummy token.
(Note: You should also try test it withPromise.reject
)
Fetching orders
Next, let's fetch an array of Order
from the order service.
type Order = {
readonly id: string;
readonly amount: number;
readonly productIds: readonly {
id: Product['id'];
}[];
};
/**
* Function that fetch the orders
*/
const fetchOrders = taskEither.tryCatchK(
// Mock the orders
({url, authToken, count, date}: FetchOrdersInput) => {
const demoOrders: Order[] = [
{
id: '1',
amount: 100,
productIds: [
{
id: 'PRODUCT_A',
},
],
},
{
id: '2',
amount: 200,
productIds: [
{
id: 'PRODUCT_B',
},
{
id: 'PRODUCT_C',
},
],
},
];
// Mock the results
return Promise.resolve(demoOrders);
},
reason => new Error(`Failed to fetch orders due to Error ${String(reason)}`)
);
The array of Order
only contains ProductIds
in this stage, we need to further map them into an array of Product
Fetching Products
Now, let’s fetch an array of Product
using ProductIds
from the product service.
type FetchProductsInput = {
url: string;
authToken: string;
ids: string[];
};
/**
* Function that fetch the products
*/
const fetchProducts = taskEither.tryCatchK(
// Mock the Product
({url, authToken, ids}: FetchProductsInput) => {
const products = ids.map(id => {
switch (id) {
case 'PRODUCT_A': {
const mockProduct: Product = {
code: 'A',
id: 'PRODUCT_A',
unit: 1,
};
return mockProduct;
}
case 'PRODUCT_B': {
const mockProduct: Product = {
code: 'B',
id: 'PRODUCT_B',
unit: 2,
};
return mockProduct;
}
case 'PRODUCT_C': {
const mockProduct: Product = {
code: 'C',
id: 'PRODUCT_C',
unit: 3,
};
return mockProduct;
}
default: {
const mockProduct: Product = {
code: 'DEFAULT',
id: 'PRODUCT_DEFAULT',
unit: 1,
};
return mockProduct;
}
}
});
return Promise.resolve(products);
},
reason => new Error(`Failed to fetch product due to Error ${String(reason)}`)
);
Creating the endpoint
Now, we have all the functions we need to fetch the orders. Let’s combine use them in the endpoint.
Suppose there are two parameters inside the req.body
— count
and date
. They are used to filter the orders by date and limit the number of orders.
Let's start with checking the count
parameter using Either.from Predicate
, and taskEither.mapLeft
to return a formatted Error. We use taskEither.apS
for the count
because we will use it when fetching the orders later on.
enum FindOrderResponseErrors {
InvalidCount = 'Invalid Count',
}
enum GeneralErrors {
InternalServerError = 'An unexpected server error was found.',
AuthTokenError = 'Unable to fetch the auth token.',
}
type ErrorResult = {
status: number;
name: FindOrderResponseErrors | GeneralErrors | Error['name'];
description: string;
};
const {count, date} = req.body;
// Suppose the authUrl, orderUrl and productUrl is stored in a env file
const authUrl = `https://YOUR_URL/auth`;
const orderUrl = `https://YOUR_URL/order`;
const productUrl = `https://YOUR_URL/product`;
const ordersResultTE = pipe(
taskEither.Do,
// Check count
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
// Fetch Auth Token ...
// Fetch Order ...
// Fetch Products ...
);
Next, we fetch the auth token. Again, we use taskEither.apS
for the authToken
because we need that to fetch the orders and products.
const ordersResultTE = pipe(
taskEither.Do,
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
taskEither.apS(
'authToken',
pipe(
fetchAuthToken({authUrl}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
})
)
),
// Fetch Order...
// Fetch Products ...
);
Now that we have the auth token, we can use it to fetch the orders.
const ordersResultTE = pipe(
taskEither.Do,
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
taskEither.apS(
'authToken',
pipe(
fetchAuthToken({authUrl}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
})
)
),
taskEither.chain(({count, authToken}) => {
return pipe(
fetchOrders({url: orderUrl, authToken, count, date}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
}),
// Fetch Products for each order ...
);
})
);
After fetching the orders, we want to map the ProductIds
into Products
by fetching the products:
const ordersResultTE = pipe(
taskEither.Do,
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
taskEither.apS(
'authToken',
pipe(
fetchAuthToken({authUrl}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
})
)
),
taskEither.chain(({count, authToken}) => {
return pipe(
fetchOrders({url: orderUrl, authToken, count, date}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
}),
taskEither.chain(orders => {
return pipe(
orders,
array.map(order =>
pipe(
fetchProducts({
ids: order.productIds.map(({id}) => id),
url: productUrl,
authToken,
}),
taskEither.map(products => {
const {productIds, ...orderWithoutProductIds} = order;
const ordersResult: OrderResult = {
...orderWithoutProductIds,
products,
};
return ordersResult;
})
)
),
taskEither.sequenceArray,
taskEither.mapLeft<Error, ErrorResult>(error => ({
name: GeneralErrors.InternalServerError,
description: error.message,
status: 500,
}))
);
})
);
})
);
The Complete version of the code (Normal function verison):
import {pipe} from 'fp-ts/function';
import * as taskEither from 'fp-ts/TaskEither';
import * as either from 'fp-ts/Either';
import * as array from 'fp-ts/Array';
type FindOrdersRequest = {
body: {
count: number;
date: Date;
};
};
export enum GeneralErrors {
InternalServerError = 'An unexpected server error was found.',
AuthTokenError = 'Unable to fetch the auth token.',
}
export enum FindOrderResponseErrors {
InvalidCount = 'Invalid Count',
}
type ErrorResult = {
status: number;
name: FindOrderResponseErrors | GeneralErrors | Error['name'];
description: string;
};
type Product = {
readonly id: string;
readonly code: string;
readonly unit: number;
};
type Order = {
readonly id: string;
readonly amount: number;
readonly productIds: readonly {
id: Product['id'];
}[];
};
type OrderResult = {
readonly id: Order['id'];
readonly amount: Order['amount'];
products: Product[];
};
type FetchOrdersInput = {
url: string;
authToken: string;
count: number;
date: Date;
};
/**
* Function that fetch the orders
*/
const fetchOrders = taskEither.tryCatchK(
// Mock the orders
({url, authToken, count, date}: FetchOrdersInput) => {
const demoOrders: Order[] = [
{
id: '1',
amount: 100,
productIds: [
{
id: 'PRODUCT_A',
},
],
},
{
id: '2',
amount: 200,
productIds: [
{
id: 'PRODUCT_B',
},
{
id: 'PRODUCT_C',
},
],
},
];
// Mock the results
return Promise.resolve(demoOrders);
},
reason => new Error(`Failed to fetch orders due to Error ${String(reason)}`)
);
type FetchProductsInput = {
url: string;
authToken: string;
ids: string[];
};
/**
* Function that fetch the products
*/
const fetchProducts = taskEither.tryCatchK(
// Mock the Product
({url, authToken, ids}: FetchProductsInput) => {
const products = ids.map(id => {
switch (id) {
case 'PRODUCT_A': {
const mockProduct: Product = {
code: 'A',
id: 'PRODUCT_A',
unit: 1,
};
return mockProduct;
}
case 'PRODUCT_B': {
const mockProduct: Product = {
code: 'B',
id: 'PRODUCT_B',
unit: 2,
};
return mockProduct;
}
case 'PRODUCT_C': {
const mockProduct: Product = {
code: 'C',
id: 'PRODUCT_C',
unit: 3,
};
return mockProduct;
}
default: {
const mockProduct: Product = {
code: 'DEFAULT',
id: 'PRODUCT_DEFAULT',
unit: 1,
};
return mockProduct;
}
}
});
return Promise.resolve(products);
},
reason => new Error(`Failed to fetch product due to Error ${String(reason)}`)
);
/**
* Function that fetch the authToken
*/
const fetchAuthToken = taskEither.tryCatchK(
// Mock the token
({authUrl}) => Promise.resolve('DEMO_AUTH_TOKEN'),
reason =>
new Error(`Failed to fetch authToken due to Error ${String(reason)}`)
);
const findOrders = async (req: FindOrdersRequest) => {
try {
const {count, date} = req.body;
// Suppose the authUrl, orderUrl and productUrl is stored in a env file
const authUrl = `https://YOUR_URL/auth`;
const orderUrl = `https://YOUR_URL/order`;
const productUrl = `https://YOUR_URL/product`;
const ordersResultTE = pipe(
taskEither.Do,
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
taskEither.apS(
'authToken',
pipe(
fetchAuthToken({authUrl}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
})
)
),
taskEither.chain(({count, authToken}) => {
return pipe(
fetchOrders({url: orderUrl, authToken, count, date}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
}),
taskEither.chain(orders => {
return pipe(
orders,
array.map(order =>
pipe(
fetchProducts({
ids: order.productIds.map(({id}) => id),
url: productUrl,
authToken,
}),
taskEither.map(products => {
const {productIds, ...orderWithoutProductIds} = order;
const ordersResult: OrderResult = {
...orderWithoutProductIds,
products,
};
return ordersResult;
})
)
),
taskEither.sequenceArray,
taskEither.mapLeft<Error, ErrorResult>(error => ({
name: GeneralErrors.InternalServerError,
description: error.message,
status: 500,
}))
);
})
);
})
);
const result = await ordersResultTE();
return result;
} catch (error) {
return {
name: GeneralErrors.InternalServerError,
description: error,
status: 500,
};
}
};
The Express.js endpoint version:
import express from 'express';
const findOrders = async (req: FindOrdersRequest, res: express.Response) => {
try {
const {count, date} = req.body;
// Suppose the authUrl, orderUrl and productUrl is stored in a env file
const authUrl = `${process.env.AUTH_URL}/auth`;
const orderUrl = `${process.env.ORDER_URL}`;
const productUrl = `${process.env.PRODUCT_URL}`;
const ordersResultTE = pipe(
//...
);
const result = await ordersResultTE();
return pipe(
result,
either.match(
({status, ...error}) => {
res.status(status).json({error: error.name});
},
orders => {
res.status(200).send(orders);
}
)
);
} catch (error) {
return res.status(500).json({
name: GeneralErrors.InternalServerError,
description: error,
status: 500,
});
}
};
export default findOrders;
What’s Next:
Congratulations! You have made it to the end of the fp-ts series 1: Container series.
In the next series, we will discuss more on Functor, Applicative Functor, Monad, and the Monoid in depth.
More about fp-ts.
Thanks for reading 🫡
Hope you enjoy this article and gain something from it. I wish it is less daunting now for you to learn Functional Programming :)
Ka Hin