2.5.0 • Published 2 years ago

@improvising/river v2.5.0

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

River Framework

River is a highly-opinionated web framework on the top of Fastify. Unlike other JavaScript frameworks, River is natively written on Typescript and applied to the modern patterns to ensure the type-safety of the framework.

Install

River Framework requires Node.js 14+. It's highly recommended to run on Unix-like operating system with Nginx.

Just simply run

yarn add @improvising/river

or

npm install @improvising/river

to install the framework.

Initialization

Since River is built on the top of Fastify web framework, we can simply use "useApp" function to init the server.

const app = useApp({
  routers: [
    /* Your routers */
  ],
  trustProxy: true,
})

const PORT = 8080
const ADDRESS = '127.0.0.1' // 0.0.0.0 if use docker

app.listen(PORT, ADDRESS, () => console.log(`Running on port ${PORT}`))

Router

Define a router in River is very easy. Let's look at an example of an user registration API to have a first impression on it.

const router = useRouter({
  prefix: '/auth',
  middlewares: [
    /* Router-level middlewares for all routes */
  ],
})

router.post('/signup', {
  schema: {
    body: Type.Object({
      countryCode: Type.Integer({ minimum: 1, maximum: 999 }),
      mobile: Type.String({
        pattern: StringPattern.Integer,
        minLength: 6,
        maxLength: 13,
      }),
      password: Type.String({ minLength: 6, maxLength: 32 }),
    }),
  },
  handler: async req => {
    const { countryCode, mobile, password } = req.body

    // ...

    return { success: true }
  },
})

For the schema and handler part, we use the totally same logic as Fastify's. However, we introduced TypeBox to our framework to define the schema (validation). With TypeBox, we can easily define typed schemas and then we can use those type inference later in our request and response interfaces in the handler function.

Beside TypeBox, we also provide StringPattern, StringType and StringTo three utility classes to help you define and convert string-related types. We also provide a LiteralType class to help you deal with the condition of multiple literals (based on Json Schema's Union Type).

Let's take a look on an example of our code

const router = useRouter({
  prefix: '/activities',
  middlewares: [AuthGuard, RateLimit({ timeWindow: 60000, maxRequests: 20 })],
})

// Get activities
router.get('/', {
  alternativePaths: ['/users/:userId/activities'],
  middlewares: [
    /* Route only middlewares */
  ],
  schema: {
    params: Type.Object({
      userId: Type.Optional(StringType.ObjectID),
    }),
    querystring: Type.Object({
      direction: LiteralType.anyOf(['from', 'to']),
      type: Type.Optional(LiteralType.anyOf(['reaction', 'postComment'])),
      limit: Type.Optional(StringType.Integer),
      before: Type.Optional(StringType.DateTime),
      after: Type.Optional(StringType.DateTime),
    }),
  },
  handler: async req => {
    const { user } = req.state
    const { direction, type } = req.query

    const limit = StringTo.Number(req.query.limit)
    const before = StringTo.Date(req.query.before)
    const after = StringTo.Date(req.query.after)

    if (!req.params.userId && !user.permissions.includes('manageEntity')) {
      throw new Forbidden()
    }

    return []
  },
})

In this part of code, you can also see that there is an alternativePath option. It's used to create a alternative path for a route (without the prefix). It's helpful when you want a handler to handle two slightly different routes without repeating your codes.

You can also find that we use middlewares here. When you set middlewares in useRouter, these middlewares will be applied to all routes under the router. When you set middlewares for a route itself, it will only be applied for itself.

It's highly recommended to also install http-errors (a Node.js module) to throw user-friendly HTTP errors, such as 403 Forbidden in this part of code.

Middleware

We use the "AuthGuard" as an example to show you how to write a middleware. Writing a middleware is just like the way you write your handlers for routers.

export const AuthGuard: Middleware = async req => {
  if (!req.headers.authorization) {
    throw new Unauthorized()
  }

  const token = req.headers.authorization.substr(7)
  const result = await AuthService.verifyToken(token)

  req.state.user = result
}

By default, we don't have a key called "user" under our request state. Therefore, it's important to create a type file for typescript to recoginize it. You may simply create a type.d.ts in your source code directory.

declare module 'fastify' {
  interface RequestState {
    user: {
      _id: string
      permissions: Permission[]
    }
  }
}

By modify the request state, you will be allowed to get them in your route handler like this:

router.get('/', {
  schema: {},
  handler: async req => {
    const { user } = req.state
    // ...
  },
})

How to feedback

If you have any question about it, you can just open an issue, create a pull request, or send an email to opensource@improvising.io for further discussions.

2.3.0

2 years ago

2.5.0

2 years ago

2.4.1

2 years ago

2.3.2

2 years ago

2.4.0

2 years ago

2.3.1

2 years ago

2.3.3

2 years ago

2.2.1

3 years ago

2.2.0

3 years ago

2.2.3

3 years ago

2.1.2

3 years ago

2.1.1

3 years ago

2.1.4

3 years ago

2.1.3

3 years ago

2.1.0

3 years ago

1.0.8

3 years ago

1.0.7

3 years ago

1.0.6

3 years ago

2.0.3

3 years ago

2.0.2

3 years ago

1.0.5

3 years ago

1.0.4

3 years ago

2.0.1

3 years ago

1.0.3

3 years ago

2.0.0

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago