npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@simspace/trout

v0.0.6

Published

(**t**ypesafe **rout**ing)

Downloads

615

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:

  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)
  
  ...
}