@simspace/trout
v0.0.6
Published
(**t**ypesafe **rout**ing)
Downloads
847
Keywords
Readme
🐟 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:
stringRC
for valid URLstring
snumberRC
fornumber
s (excludingNaN
andbigint
s)booleanRC
forboolean
sstringLiteralRC
andnumberLiteralRC
for literalstring
andnumber
values (or unions of those values)dateRC
forDate
s (encoded in the URL as a string in simplified extended ISO format, the result of callingDate.prototype.toISOString()
)
There are also a few useful codec combinators for building more complex codecs:
newtypeRC
takes anIso
for theNewtype
(à lanewtype-ts
), and an underlying route codec, will map the codec's type into aNewtype
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)tupleRC
takes a pair of codecs, and will encode and decode a two-element array as a tupleliteralRC
takes a set of literal values and anEq
instance for those values, will encode and decode as a union of those values (stringLiteralRC
andnumberLiteralRC
are implemented withliteralRC
)
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)
...
}