How domain-functions improves the (already awesome) DX of Remix projects?
Tags: remix, typescript, react, architecture
Published: 2022-05-06 23:33:21

At [Seasoned](https://seasoned.cc) we are absolutely in love with [Remix](https://remix.run/). We have been working with it for a few months and we really think it is a great tool.
But as with anything we'd use, we've found a few areas where we think the _out-of-the-box_ approach falls short.

In this post, I'm gonna talk about some of them and show how we are solving those problems with our new library: [domain-functions](https://github.com/SeasonedSoftware/domain-functions).

## Plumbing
As soon as any project starts to get a little serious a demand for a structured architecture starts to emerge. On almost every example of Remix's actions and loaders out there, we can see some boilerplate we call plumbing.

For instance, take a look at the action on [this chunk of code](https://github.com/remix-run/remix-jokes/blob/main/app/routes/jokes/new.tsx#L23-L74) of the Jokes App. A lot is going on! And this is usually how Remix Apps look, relaying a lot of responsibility to the controllers.

Let's put the common loader/action's responsibilities in perspective:
- A) Extracting data out of the request, be it from the query string, the body, the url, request headers, etc.
- B) Assert that this data is in the right format, maybe doing some data munging after that.
- C) Doing some side effects, such as inserting data into the database, sending emails, etc.
- D) Returning a different `Response` depending on the result of previous steps.
- E) There's also a need to manually [maintain the types](https://github.com/remix-run/remix-jokes/blob/main/app/routes/jokes/new.tsx#L35-L42) of your Responses aligned with what the UI expects, we'll talk more about it later in this post.

As you've probably guessed already, we think about loaders and actions as controllers. And we think controllers should only "speak HTTP". In that mindset, we'd refactor the list above to only steps **A and D**. Steps **B and C** are what we call _Business logic_, the code which makes the project unique.

And at Seasoned we like to separate this code into well defined/tested/typed _domains_.

### So how would we decouple that business logic with `domain-functions`?

First, we'd write Zod schemas for both the user input and the [environment](https://github.com/SeasonedSoftware/domain-functions#taking-parameters-that-are-not-user-input) data:
```ts
// app/domains/jokes.server.ts
const jokeSchema = z.object({
  name: z.string().min(2, `That joke's name is too short`),
  content: z.string().min(10, 'That joke is too short'),
})

const userIdSchema = z.string().nonempty()
```
Then we'd write the business logic using those schemas:
```ts
// app/domains/jokes.server.ts
import { makeDomainFunction } from 'domain-functions'
// ...

const createJoke = makeDomainFunction(jokeSchema, userIdSchema)
  ((fields, jokesterId) =>
    db.joke.create({ data: { ...fields, jokesterId } })
  )
```

And lastly we'd write the controller's code:
```ts
// app/routes/jokes/new.tsx
import { inputFromForm } from 'domain-functions'
// ...

export async function action({ request }: ActionArgs) {
  const result = await createJoke(
    await inputFromForm(request),
    await getUserId(request),
  )
  if (!result.success) {
    return json(result, 400)
  }
  return redirect(`/jokes/${result.data.id}?redirectTo=/jokes/new`)
}
```

Now, let's rewrite [this loader](https://github.com/remix-run/remix-jokes/blob/main/app/routes/jokes/index.tsx#L9-L32). We start with the domain:
```ts
// app/domains/jokes.server.ts
const getRandomJoke = makeDomainFunction(z.null(), userIdSchema)
  (async (_i, jokesterId) => {
    const count = await db.joke.count()
    const skip = Math.floor(Math.random() * count)
    return db.joke.findMany({ take: 1, skip, where: { jokesterId } })
  })
```
Then, the loader:
```tsx
// app/routes/jokes/index.tsx
export async function loader({ request }: LoaderArgs) {
  const result = await getRandomJoke(
    null,
    await getUserId(request)
  )
  if (!result.success) {
    throw new Response('No jokes to be found!', { status: 404 })
  }
  return json(result.data)
}
```
Do you see a pattern emerging on those controllers?
#### If you want to see the full Jokes App implemented with `domain-functions` check [this PR diff](https://github.com/gustavoguichard/remix-example-jokes/pull/2)!.

## Keeping a Pattern
With controllers doing so much, it is harder to keep a standard way to structure your successful and failed responses.

_Should we be adding `try/catch` in our controllers?_
_How are we going to return input errors and how to differ 'em from actual runtime or service errors?_

`domain-functions` does that for you, by having a structured way to present data and errors, you can be sure the responses will always be consistent. You also don't need to work with `try/catch` blocks, we can actually just throw errors in our domain functions, the same pattern Remix uses internally, so you can only write the happy path of your domain functions and `throw` errors to ensure type safety:
```tsx
const getJoke = makeDomainFunction(z.object({ id: z.string().nonempty() }), userIdSchema)
  (async ({ id }, jokesterId) => {
    const joke = await db.joke.findOne({ where: { id, jokesterId } })
    if (!joke) throw new Error('Joke not found')
    return joke
  })
```
On the domain function above, if successful the response will look like this:
```ts
const result = {
  success: true,
  data: { id: 1, name: 'Joke name', content: 'Joke content' },
  inputErrors: [],
  environmentErrors: [],
  errors: [],
}
```
Otherwise it'll look like this:
```ts
const result = {
  success: false,
  inputErrors: [],
  environmentErrors: [],
  errors: [{ message: 'Joke not found' }],
}
```

Now that we reduced the boilerplate out of our actions/loaders and found an architectural pattern, it is easy to start creating our own little abstractions on top of them.

```tsx
// app/lib/index.ts
function queryResponse<T>(result: T) {
  if (!response.success)
    throw new Response('Not found', { status: 404 })
  return json<T>(result.data)
}

// app/routes/jokes/$id.tsx
export async function loader({ params }: LoaderArgs) {
  return queryResponse(await getJoke(params))
}
```

## Testing
Now imagine you need to thoroughly test that original code.

Currently, there's no easy way to do it, without kind of mocking the Router API. The solution usually lies in E2E testing.

We'd like to do unit and integration tests in our business logic, though. Now that our domain functions are just functions that receive data and return data, we can easily write them:

```tsx
// Soon we'll be writing about how we set up our test database.
// For the sake of brevity, pretend there's already a DB with jokes
describe('getRandomJoke', () => {
  it('returns a joke for the given userId', async () => {
    const { user, jokes } = await setup()
    const result = await getRandomJoke(null, user.id)
    if (!result.success) throw new Error('No jokes to be found!')
    expect(jokes.map(({ id }) => id)).toInclude(result.data.id)
  })
})
```
If you ever tried to do such sort of testing on your Remix routes you are probably happy about what you've just seen.

## Parsing structured data from Forms
Ok, this is not a Remix limitation but rather a "limitation" of the [`FormData`](https://developer.mozilla.org/pt-BR/docs/Web/API/FormData) API.
It is often useful to parse structured data from forms, for example, when you have nested forms or repeatable fields.

`FormData` can only work with flat structures and we need to know the structure of the data beforehand to know if we should call `formData.get('myField')` or `formData.getAll('myField')`. It arguably does not scale for complex forms.

By structured data I mean making the `FormData` from this form:

```html
<form method="post">
  <input name="numbers[]" value="1" />
  <input name="numbers[]" value="2" />
  <input name="person[0][email]" value="john@doe.com" />
  <input name="person[0][password]" value="1234" />
  <button type="submit">
    Submit
  </button>
</form>
```

Be interpreted as this:
```json
{
  "numbers": ["1", "2"],
  "person": [{ "email": "john@doe.com", "password": "1234" }]
}
```

Well, `domain-functions` leverages [`qs`](https://github.com/ljharb/qs#parsing-objects) to do that conversion for you with `inputFromForm`:
```tsx
import { inputFromForm } from 'domain-functions'

const result = await myDomainFunction(await inputFromForm(request))
```
The library exposes [other utilities](https://github.com/SeasonedSoftware/domain-functions#extracting-input-values-for-domain-functions) for doing that sort of job. 

## End-to-end Type Safety and composition
One of the biggest complaints about Remix (and NextJS) is the lack of end-to-end type safety.
Having to [maintain the types](https://github.com/remix-run/remix-jokes/blob/main/app/routes/jokes/new.tsx#L35-L42) by hand is boring and prone to errors. We wanted an experience as good as [tRPC](https://github.com/trpc/trpc) and now that our domain functions have knowledge of your domain's I/O we are in the same situation as tRPC [as stated by its author](https://twitter.com/colinhacks/status/1490482114759856131?t=bvXHelMxmgXVtdJT3Ql5yw&s=19):
> As for output types—tRPC trusts the return type inferred from your resolver which is reasonable IMO. These types largely originate from sources (Prisma queries, etc) that provide trustworthy types.

_BTW: Colin is also the author of [Zod](https://github.com/colinhacks/zod) and a bunch of nice projects, we can't overstate how much we like his projects._

When working with `domain-functions`, you don't need to be writing types by hand as they are infered out of the domain functions. The following GIF shows the DX of this workflow:

![type safety](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iqo9xxr74oqi4ho2qicz.gif)

## Composition

When we started working on this project, we didn't realize we'd achieve such a good type safety nor did we planned to create a _perfect arithmetic progression_ by always expressing the return of our Domain Functions as `Promise<Result<MyData>>`.

So when we were confronted with the problem of working with multiple domains in a single loader without changing our controller's architecture, the answer was right in front of us: creating functions to compose multiple Domain Functions, resulting in... a Domain Function!

So far we've created [3 functions](https://github.com/SeasonedSoftware/domain-functions#combining-domain-functions) enabling us to code like this:

```tsx
import { all, map, pipe } from 'domain-functions'
import { a, b, c, d, e } from './my-domain-functions.server'

// Transform the successful data, ex:
// { success: true, data: "foo" } => { success: true, data: true }
const x = map(a, Boolean)
// Create a domain function that is a sequence of Domain Functions
// where the output of one is the input of the next
const y = pipe(x, b, c)
// Create a domain function that will run all the given domain functions
// in parallel with `Promise.all`
const getData = all(d, e, y)

export async function loader({ params }: LoaderArgs) {
  return queryResponse(await getData(params))
}

export default function Component() {
  const [
    dataForD, dataForE, dataForY
  ] = useLoaderData<typeof loader>()
  // ...
}
```

All that logic and our _loader's boilerplate_ is untouched!

This other GIF showcases the DX for compositions:
![composition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59z78ozth6lt9jsth9nh.gif)

## Conclusion
We are pretty excited about this project and we hope you'll like it.
We've been battle-testing it and can't wait to see the feedback of the community.

If you felt lost about the examples above, check out the [documentation](https://github.com/SeasonedSoftware/domain-functions#table-of-contents) and [examples](https://github.com/SeasonedSoftware/domain-functions/tree/main/examples) of `domain-functions`.

We are gonna be over the moon if it helps more teams to structure their projects and we are looking forward to your feedback/contributions.