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

@alien-rpc/service

v0.2.0

Published

Service library for alien-rpc

Downloads

107

Readme

@alien-rpc/service

This package is used by your backend code to define your API routes. It's a simplistic library without many features, as it relies on other libraries (e.g. hattip, typebox, and pathic) to handle the heavy lifting.

The rest of this document will teach you how to define routes, document them, validate their input/output data, and expose them over HTTP.

 

Defining routes

First, let's define a GET route. We'll need a path pattern (a string literal) and a route handler (a plain old JavaScript function).

import { route } from '@alien-rpc/service'

export const getUser = route.get('/users/:id', async ({ id }) => {
  // TODO: return some JSON-compatible data
})

For more on the path pattern syntax, see the “Path patterns” section of the Pathic readme.

Your routes can be declared anywhere in your codebase, as long as they are exported and the alien-rpc generator is told where to find them. It's best practice to have dedicated modules for your routes (i.e. avoid declaring them alongside other exports that aren't routes).

If you prefer all-caps HTTP methods, you can use the exported route.GET function instead. There is no functional difference, but it's a matter of personal preference.

Route Arguments

If a route has path parameters, its handler will have 3 arguments (pathParams, requestData, ctx). Otherwise, it will have 2 (requestData, ctx). The requestData is an object of either the route's search parameters (for GET/DELETE/HEAD routes) or its JSON body (for POST/PUT/PATCH routes). The ctx argument is the request context, as defined by hattip.

Path parameters

If a route has exactly 1 path parameter, its pathParams argument will be a single value. If a route has multiple path parameters, pathParams will be an array of values. If you don't explicitly type the pathParams argument, each parameter value is typed as a string.

const getUser = route.get('/users/:id', async id => {
  // typeof id === 'string'
})

const getUserInGroup = route.get(
  '/groups/:groupId/users/:userId',
  async ([groupId, userId]) => {
    // typeof groupId === 'string'
    // typeof userId === 'string'
  }
)

Supported HTTP methods

The following HTTP methods are supported:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • OPTIONS
    No need to manually define this route, as it's handled internally.
  • HEAD
    While you can define a HEAD route, your GET routes will also match HEAD requests. You can check for this in your route handler via ctx.request.method === "HEAD". Even if your route handler returns a response body, it will be ignored for HEAD requests.

 

Documenting routes

Routes can be documented like any TypeScript function.

/**
 * Retrieve the public profile for a given user.
 *
 * @param id - The ID of the user to retrieve.
 */
const getUser = route.get('/users/:id', async id => {
  // ...
})

This documentation is extracted by @alien-rpc/generator and included with the client's type definitions.

Currently, this works for routes, but not their path parameters or request data. This feature is being tracked in #3 (contributions welcome).

 

Runtime validation

TypeScript types are used by @alien-rpc/generator to determine how your route's path parameters and request data should be validated at runtime.

So if your getUser route is expecting the id path parameter to be a number and the includePrivate search parameter to be a boolean, you can define it like this:

import { route } from '@alien-rpc/service'

const getUser = route.get(
  '/users/:id',
  async (id: number, searchParams: { includePrivate?: boolean }) => {
    // ...
  }
)

Note that you can also explicitly type your request data, which in case you forgot, is the 2nd argument to your route handler that represents the JSON request body (for POST/PUT/PATCH routes) or search parameters (for GET/HEAD/DELETE routes).

Path parameter limitations

Path parameters can only be one of the following types: string, number, or an array of those types. In the case of an array, the path parameter will be split by any / characters within it, which is probably only useful for wildcard parameters (e.g. /files/*filename).

Date parsing

You may use the Date type for your “request data” to parse a string into a Date object. As implied in the previous section, this is not supported for path parameters.

This even works for POST/PUT/PATCH request bodies, which use JSON encoding. Basically, the date will be serialized to the ISO-8601 format during transport, and parsed back into a Date object upon arrival.

Type constraints

Sometimes, TypeScript types aren't strict enough for your use case. For example, you might expect the id parameter of your getUser route to be an integer greater than 0.

For this, you can use the “type tags” feature:

import { route, t } from '@alien-rpc/service'

const getUser = route.get(
  '/users/:id',
  async ({ id }: { id: number & t.MultipleOf<1> & t.Minimum<1> }) => {
    // ...
  }
)

Type constraints are supported everywhere TypeScript types are supported, including path parameters, request data, and response data.

The Type Constraints page has more information on the available constraints.

 

Request context

The request context is the last argument of your route handler. It's an object containing information about the incoming request, such as the request method, headers, and URL. See here for a complete list of properties and methods in the RequestContext type.

Note that your route handler always receives an object representing the request data (either search parameters or JSON body). Therefore, to access the request context, you need to declare an argument name for the request data first. See _data in the example below:

export const getApplStockPrice = route.get(
  '/stocks/appl',
  async (_data, ctx) => {
    ctx.url // => [object URL]
    ctx.request.url // => "/stocks/appl"
    ctx.request.headers // => [object Headers]
  }
)

Response manipulation

The request context contains a response object property with a status number and a headers object. You can modify these properties to customize the HTTP response.

export const getFile = route.get('/files/:id', async (id, _, ctx) => {
  ctx.response.status = 200 // Note: The default status is 200

  ctx.response.headers.set('Content-Type', 'application/pdf')
  ctx.response.headers.set(
    'Content-Disposition',
    'attachment; filename="file.pdf"'
  )

  return await getFileContents(id)
})

 

Exposing routes over HTTP

The compileRoutes function creates a middleware function that can be used with hattip. It expects an array of route definitions, which are located wherever you set --serverOutFile to when running @alien-rpc/generator through the CLI (it defaults to ./server/generated/api.ts).

import { compose } from '@hattip/compose'
import { compileRoutes } from '@alien-rpc/service'
import routes from './server/generated/api.js'

export default compose(
  loggingMiddleware(), // <-- runs before your routes
  compileRoutes(routes),
  ssrMiddleware() // <-- runs after your routes
)

[!NOTE] In the example above, the loggingMiddleware and ssrMiddleware are hypothetical. Creating your own middleware is as easy as declaring a function (optionally async) that receives a RequestContext object and returns one of the following: a Response object, any object with a toResponse method, or nothing (aka void).

If you save the code above in the ./server/handler.ts module, you could start your server in the ./server/main.ts module like this:

import { createServer } from '@hattip/adapter-uwebsockets'
import handler from './handler.js'

createServer(handler).listen(3000, 'localhost', () => {
  console.log('Server listening on http://localhost:3000')
})

 

Error handling

Currently, error handling is performed by the compileRoutes function.

Errors thrown by your route handlers are assumed to be unintentional, unless you throw an HttpError instance like so:

import { route, UnauthorizedError } from '@alien-rpc/service'

export const getPrivateProfile = route.get('/users/:id/private', async id => {
  throw new UnauthorizedError()
})

For more details, see the HTTP errors page.

 

Streaming responses

If you want to stream JSON to the client, define your route handler as an async generator:

import { route } from '@alien-rpc/service'

export const streamPosts = route.get('/posts', async function* () {
  yield { id: 1, title: 'First post' }
  yield { id: 2, title: 'Second post' }
})

This takes advantage of the JSON Text Sequence format. Any JSON-compatible data can be yielded by your route handler. This allows the client to start receiving data before the route handler has finished executing.

Pagination

The paginate function allows you to provide pagination links in the response. This is only supported for routes whose handler is an async generator.

Please note that only GET routes support pagination.

The paginate function takes two arguments:

  • route: A reference to the current route (via this) or another route (by the identifier you exported it with)
  • links: An object with the prev and next pagination links, which must provide an object containing path parameters and/or search parameters for the next/previous set of results

You must return the paginate function's result from your route handler.

import { route, paginate } from '@alien-rpc/service'

export const streamPosts = route.get(
  '/posts',
  async function* ({ offset, limit }: { offset: number; limit: number }) {
    let count = 0
    for await (const post of db.posts.find({ offset, limit })) {
      yield post
      count++
    }
    return paginate(this, {
      prev: offset > 0 ? { offset: offset - limit, limit } : null,
      next: count === limit ? { offset: offset + limit, limit } : null,
    })
  }
)

Pagination is an optional feature. It not only supports an offset+limit style of pagination, but any other kind, like cursor-based pagination. When calling a paginated route through the alien-rpc client, two methods (previousPage and nextPage) are added to the ResponseStream object returned by that route's client method.

Streaming arbitrary data

If you need to stream data that isn't JSON, your route's handler needs to return a Response object whose body is a ReadableStream.

import { route } from '@alien-rpc/service'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { Readable } from 'node:stream'

export const downloadFile = route.get('/files/*filename', async filename => {
  const fileStream = fs.createReadStream(path.join('./uploads', filename))

  return new Response(Readable.toWeb(fileStream), {
    headers: {
      'Content-Type': 'application/octet-stream',
      'Content-Disposition': `attachment; filename="${filename}"`,
    },
  })
})

 

Conclusion

This concludes the documentation for @alien-rpc/service. Be sure to check out the documentation for the other packages in this library:

If you still have questions, please open an issue and I'll do my best to help you out.