0.0.6 • Published 12 months ago

@simspace/trout v0.0.6

Weekly downloads
-
License
Apache-2.0
Repository
gitlab
Last release
12 months ago

🐟 trout

(typesafe routing)

Install

NPM

npm install @simspace/trout

Yarn

yarn add @simspace/trout

Making a route

To make a route, you must either extend an existing path, or extend the RootPath. You do this with the path function, passing the segments that you wish to extend.

import * as tr from 'trout'

const usersRoute = pipe(
  tr.RootPath,
  tr.path('users')
)

Here, usersRoute represents the path: /users.

Route parameters

We can add route parameters by using the param function:

import * as tr from 'trout'

const userProfileRoute = pipe(
  usersRoute,
  tr.param('userId', tr.stringRC)
)

Here, we've added a route parameter named userId, and passed the stringRC route codec to indicate that this parameter's type is a string. When encoding and decoding this route, trout will use encodeURIComponent and decodeURIComponent respectively to ensure any string passed into the route constructor is serializable as a valid URL string. More complex codecs (for non-string types) can be built upon this, as we'll see below.

Query parameters

We can add query parameters to a route with the queryParam/queryParamOp functions:

const usersRoute = pipe(
  tr.RootPath,
  tr.path('users'),
  tr.queryParam('page', tr.numberRC),
  tr.queryParamOp('pageSize', tr.numberRC)
)

The queryParam function takes the name of the query parameter, and the codec, which determines the type of the parameter. Here, we expect to get a parameter named page which will be attempted to be decoded as a number. queryParamOp works similarly to queryParam except that it specifies an optional query parameter (encoded using Option from fp-ts).

Building URLs

Now that we have a route, we can construct URLs from it:

tr.encodeUrl(usersRoute)({
  query: {
    page: 5,
    pageSize: O.none
  }
}) // gives us: "/users?page=5"

The encodeUrl function ensures we have the correct types for all of our parameters.

Extending existing routes

A route can be extended further with more segments via the path (or param) function. Adding another path segment will invalidate all query parameters from the previous route. Here, usersRoute has a page parameter, but since userProfileRoute added another route segment (via param), those query parameters are not included in userProfileRoute.

const userProfileRoute = pipe(
  usersRoute,
  tr.param('userId', tr.stringRC)
)

Route Codecs

To encode and decode parameters in a typesafe manner, trout uses route codecs. trout provides a number of basic codecs out of the box:

  1. stringRC for valid URL strings
  2. numberRC for numbers (excluding NaN and bigints)
  3. booleanRC for booleans
  4. stringLiteralRC and numberLiteralRC for literal string and number values (or unions of those values)
  5. dateRC for Dates (encoded in the URL as a string in simplified extended ISO format, the result of calling Date.prototype.toISOString())

There are also a few useful codec combinators for building more complex codecs:

  1. newtypeRC takes an Iso for the Newtype (à la newtype-ts), and an underlying route codec, will map the codec's type into a Newtype
  2. arrayRC takes a route codec, and returns a route codec for a homogenous array of those values (only for query parameters, represented in the URL as comma-separated values)
  3. tupleRC takes a pair of codecs, and will encode and decode a two-element array as a tuple
  4. literalRC takes a set of literal values and an Eq instance for those values, will encode and decode as a union of those values (stringLiteralRC and numberLiteralRC are implemented with literalRC)

trout's RouteCodec is implemented in terms of io-ts/Codec, so combinators from that library may also be used in constructing custom codecs.

The newtypeRC route codec can be used to decode parameters with a supplied iso function. Here's an example where the userId parameter is specified as a UserIdM (instead of just a string):

interface UserIdM extends Newtype<{readonly UserIdM: unique symbol}, string> {}
const isoUserId = iso<UserIdM>()

const userProfilePath = pipe(
  usersRoute,
  tr.param(
    'userId',
    tr.newtypeRC(isoUserId, tr.stringRC)
  )
)

Now, when we try to construct this route, we need to supply a UserIdM:

tr.encodeUrl(userProfilePath)({
  route: {
    page: 5,
    userId: isoUserId.wrap('bob')
  }
})

Decoding values from a URL

The decodeUrl function takes a route and an URL string, giving us back an Either, having a Left of errors (if the URL doesn't match this route), or a Right of the decoded values:

const result1 = tr.decodeUrl(
  userProfilePath
)('/users/5/bob')
// right({
//   routeParams: {
//     page: 5,
//     userId: 'bob'
//   },
//   queryParams: {}
// })

const result2 = tr.decodeUrl(
  userProfilePath
)('/users/bob')
// left('Path segment count mismatch')

const result3 = tr.decodeUrl(
  userProfilePath
)('/user/5/bob')
// left('Path segment users does not match user')

Usage with React Router

The getRouteString function will provide a react-router-compatible string which can be used in the path prop of a react router Route component.

<Route>
  <Route route={tr.getRouteString(usersRoute)} component={UsersList} />
  <Route route={tr.getRouteString(userProfilePath)} component={UsersList} />
</Route>

Inside the component, you can decode the URL by combining the location with the search values:

const UsersList = ({location}: UsersListProps) => {
  const decoded = tr.decodeUrl(usersRoute)(location.pathname + location.search)
  
  ...
}
0.0.6

12 months ago

0.0.5

1 year ago

0.0.4

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago