Bigriver
Bigriver is a highly-opinionated web framework on the top of Node.js, Redis and MongoDB. Unlike other JavaScripts frameworks, Bigriver is natively written on Typescript and applied to the modern patterns to ensure the type-safety of the framework.
Install
Bigriver requires Node.js 14+, Redis and MongoDB. It's highly recommended to run Bigriver on Unix-like operating system with Nginx.
Just simply run
yarn add bigriver
or
npm install bigriver
to install the framework.
Initialization
Bigriver uses a series of init functions like React's hook functions for its services.
Initialize app (Fastify server)
Since Bigriver is built on the top of Fastify web framework, we can simply use "useApp" function to init the server.
const app = useApp({
routers: [
],
trustProxy: true,
})
const PORT = 8080
const ADDRESS = '127.0.0.1'
app.listen(PORT, ADDRESS, () => console.log(`Running on port ${PORT}`))
Initialize MongoDB Connection
Simply use a mongodb uri to connect to it. Since Bigriver follows the best pattern we considered for our scenes, we don't provide any more options for MongoDB connection to avoid deprecations and performance decays.
await useMongoDB('mongodb://[URI]')
Initialize Redis Connection
Redis connection options are the same as IORedis' connection options.
await useRedis({
host: '',
password: '',
port: 6379,
keyPrefix: '',
enableAutoPipelining: true,
reconnectOnError: () => 2,
})
Router
Define a router in Bigriver 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.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 })],
})
router.get('/', {
alternativePaths: ['/users/:userId/activities'],
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
},
})
MongoDB & Data Model
In typical Web frameworks, there is usually a concept called ORM (Object-relational Mapping) or ODM (Object-document Mapping). However, the implementation of ORMs or ODMs usually brings performance decays and makes everything complicated. In Bigriver, we don't use any relational mapping. Instead, we use TypeScript's language features to implement a Virtual Model
. Let's quickly look at an example:
export interface Place extends BaseModel, Timestamps {
name: string
description: string
location: GeoJsonPoint
owner: Ref<User>
}
export const PlaceModel = useModel<Place>({
collection: 'place',
timestamps: true,
sync: true,
indexes: [
[{ owner: 1 }],
[{ 'location': '2dsphere' }],
],
})
const asyncfunc = async () => {
await PlaceModel.create({
name: 'New Place',
description: 'Something',
location: {
type: 'Point',
coordinates: [0, 0],
},
owner: ,
})
const places = await PlaceModel.find({}).toArray()
await PlaceModel.populate(places, [
{
model: UserModel,
path: 'owner',
select: ['nickname', 'avatarUrl'],
},
])
console.log(places)
}
asyncfunc()
Basically, the query functions are just the same syntax as Node.js MongoDB driver (Because we built Bigriver on the top of it). Our useModel
function will create a virtual model for you. The virtual model will automatically create the indexes and handle timestamps for your model. In this case, PlaceModel.populate
will help you to join references based on BSON ObjectIDs. The usage of this populate
function is similar to the one in Mongoose.
How to feedback
If you have any question about Bigriver, you can just open an issue, create a pull request, or send an email to opensource@topos.company for further discussions.