More advanced pipeline composition

Patrick Roza
2 min readJun 11, 2019


When we add more functional composition tools to our belt, we can start composing usecase pipelines that are both terse and descriptive.

Part of Functional Programming: Clean architecture and DDD series


  • From previous article: map: (value => newValue) => Result<newValue, ...>
  • flatMap: (value => newResult) => newResult
  • toTup: (value => newValue) => readonly [newValue, value]
  • tee: (value => any) => Result<value, ...>
  • resultTuple: (...[Result<..., ...>]) => Result<readonly [value, value2, ...], error[]>


type CreateError = CombinedValidationError | InvalidStateError | ValidationError | ApiError | DbError

// ({ templateId: string, pax: Pax, startDate: string }) => Result<TrainTripId, CreateError>
flatMap(validateCreateTrainTripInfo), // R<{ pax: PaxDefinition, startDate: FutureDate, templateId: TemplateId}, CombinedValidationError>
flatMap(toTup(({ templateId }) => getTrip(templateId))), // R<[TripWithSelectedTravelClass, { pax... }], ...>
map(([trip, proposal]) => TrainTrip.create(proposal, trip)), // R<TrainTrip, ...>
tee(db.trainTrips.add), // R<TrainTrip, ...>
map(trainTrip =>, // R<TrainTripId, ...>

The validateCreateTrainTripInfo function:

// ({ templateId: string, pax: Pax, startDate: string}) => Result<({ pax: PaxDefinition, startDate: FutureDate, templateId: TemplateId }), CombinedValidationError>
flatMap(({ pax, startDate, templateId }) =>
map(([pax, startDate, templateId]) => ({
pax, startDate, templateId,

Both are taken from usecases/createTrainTrip.ts

This validator facilitates domain level validation, not to be confused with REST level DTO validation. It prepares the validated DTO data for input to the domain factory TrainTrip.create. These domain rules are neatly packaged in the Value objects FutureDate and PaxDefinition, reducing complexity and knowledge creep in the factory.

Again, if tc39 proposal-pipeline-operator would land, we can write more terse and beautiful code.


We’re wrapping each ValidationError into a FieldValidationError, so that we have the name of the field in the error context, then at the end we combine them into a single error, which can be easily examined and serialized to e.g JSON on the REST api to be consumed and examined by the client.


if (err instanceof CombinedValidationError) {
ctx.body = {
fields: combineErrors(err.errors),
ctx.status = 400

const combineErrors = (ers: any[]) => ers.reduce((prev: any, cur) => {
if (cur instanceof FieldValidationError) {
if (cur.error instanceof CombinedValidationError) {
prev[cur.fieldName] = combineErrors(cur.error.errors)
} else {
prev[cur.fieldName] = cur.message
return prev
}, {})


As always you can also find the full framework and sample source at patroza/fp-app-framework

What’s Next

Next in the series, I plan to examine the question: “When to return errors, and when to throw them?”

Originally published at