BackendStack21 / restana

Restana is a lightweight and fast Node.js framework for building RESTful APIs.
MIT License
467 stars 27 forks source link

Express like Router Object #62

Closed campie closed 4 years ago

campie commented 4 years ago

Hi, first of all, thank you very much for this project and for being so kind when replying to the issues here.

With express js, I use router objects outside the main server.js file; from this file I require all the router objects and apply them by using app.use('/prefix', routerObj).

My main concern is code organization but I don't know if using router objects is the best way to do it with express.

Anyways, it seems restana does not implement a Router Object like express (does it?).

So, given all this, do you have any suggestions for code organization using restana?

Thank you!

jkyberneees commented 4 years ago

Hi @campie, first of all, thanks for your kind words in regards to the project.

You are right, restana does not support the nested routers abstraction at the moment. The main reason for not implementing this was to reduce the risk of performance impact.

There are some utility functions we can place on top of restana to simulate a router component. I want to ask, on your case is this only about routes prefixes or also middlewares execution? In case you are able to follow up with an example server code I can bring in some suggestions for better code organisation.

Thanks and looking forward to your feedback.

campie commented 4 years ago

Hi, @jkyberneees

Thanks for replying this fast!

I came up with a solution to organize my request handler functions so the folder structure maps to the requests' url. I'm building a RPC-like api.

Folder structure example:

db
    index.js
lib
    loadRoutes.js
routes
    api
        getAllUsers.js
        getProfile.js
        ...
    auth
        login.js
        ...
server.js

server.js

const restana = require('restana')
const loadRoutes = require('./lib/loadRoutes')
const db = require('./db')

const run = async () => {

    await db()

    const app = restana()

    loadRoutes(app)

    console.log(app.routes())

    app.start(3000)
}

run()

lib/loadRoutes.js

const fs = require('fs')

module.exports = function (app, prefix = '') {
    resolveRoutes(prefix).forEach(route => {
        const fn = require(route.modulePath)
        fn(app, route.path)
    })
}

function resolveRoutes(prefix = '', subFolder = '') {
    const files = fs.readdirSync(__dirname + '/../routes' + subFolder)

    return files.reduce((acc, file) => {

        const [fileName, extension] = file.split('.')

        if (extension === 'js') {

            const modulePath = __dirname + '/../routes' + subFolder + '/' + fileName

            const path = fileName !== 'index'
                ? prefix + subFolder + '/' + fileName
                : prefix + subFolder
                    ? prefix + subFolder
                    : '/'

            acc.push({
                modulePath,
                path
            })
        } else {
            const nestedSubFolder = subFolder + '/' + fileName
            acc.push(...resolveRoutes(prefix, nestedSubFolder))
        }

        return acc

    }, [])
}

routes/api/getAllUsers.js

const db = require('../../db')

module.exports = function (app, path) {
    app.get(path, (req, res) => {
        res.send(db().users.getAll())
    })
}

Side note: with express, I can do app.set('db', dbInstance) and then access req.app.get('db') inside a req handler function. In the example above, in every req handler function file where I need the dbInstance, I have to require it. What do you think about this?

When I run server.js, I get the following routes: [GET]/api/getAllUsers [GET]/api/getProfile [POST]/auth/login ...

It works very well!

The problem now is applying middlewares to specific paths. It seems app.use('/api', middleware) can't be used. So I can't do the following in server.js:

//...
const run = async () => {

    await db()

    const app = restana()

    // these two lines break the app
    app.use('/api', authMiddleware)
    app.use('/auth', rateLimitMiddleware)

    loadRoutes(app)

    console.log(app.routes())

    app.start(3000)
}

run()

One solution could be adding Route level middlewares. I may change the loadRoutes() function so that whenever a middlewares.js file is found in a subfolder inside routes folder, the middlewares are injected in all request hadler functions in the current folder and nested folders.

The request handler functions would become something like this:

module.exports = function (app, path, middlewares) {
    app.get(path, ...middlewares, (req, res) => {
        res.send(db().users.getAll())
    })
}

I would love to know what you think about all this. Maybe you have something better to suggest.

Thank you very much!

jkyberneees commented 4 years ago

Hi @campie, I really appreciate your effort on getting upon some of the missing features in restana. For global middlewares at prefix level I would recommend the following module: https://www.npmjs.com/package/middleware-if-unless

I am planning a big refactoring for version 4 of restana, more express compatibility with still 2-3x performance gain. Please keep tuned.

Best Regards, Rolando

campie commented 4 years ago

Hi @jkyberneees,

I'm closing this issue because everything is working!

Thank you for suggesting the middleware-if-unless module. It's definitely useful for my use case.

Instead of using middleware-if-unless though, I have refactored my loadRoutes function so that every request handler function receives four arguments: app, path, middlewares (array) and context (an object into which I can inject a DB reference, for example).

routes/api/getAllUsers.js

module.exports = function (app, path, middlewares, context) {
    app.get(path, ...middlewares, (req, res) => {
        res.send(context.db.users.getAll())
    })
}

server.js

const restana = require('restana')
const loadRoutes = require('./lib/app/loadRoutes')
const connectDb = require('./db')
const cookieParser = require('cookie-parser')
const bodyParser = require('body-parser')
const authMid = require('./middlewares/auth')
const limitById = require('./middlewares/limitById')

const run = async () => {

    const db = await connectDb()

    const context = {
        db
    }

    loadRoutes({
        app,
        folder: '/api',
        middlewares: [
            cookieParser(),
            authMid,
            limitById,
            bodyParser.json()
        ],
        context
    })

    loadRoutes({
        app,
        folder: '/auth',
        middlewares: [
            bodyParser.json()
        ],
        context
    })

    console.log(app.routes())

    app.start(process.env.APP_PORT)
}

run().catch(console.log)

I'm looking forward to playing with restana version 4.

Thank you very much for all you've been doing!

jkyberneees commented 4 years ago

You rock!