2

Context: I am migrating from Nuxt to Quasar framework, and initially I want to port my full-stack apps from Nuxt to Quasar SSR mode. When using Nuxt, apps are full-stack by default, we just need to place backend related files in the server-directory (see https://nuxt.com/docs/guide/directory-structure/server#server-directory).

Question: I have already created my first Quasar application and added the SSR mode, but I could not find anything similar to Nuxt server-directory :-( So the question is: Does Quasar offer something similar to Nuxt server-directory? How to generate a Quasar SSR full-stack application?

3 Answers3

3

I don't think Quasar offers something similar to server-directory out-of-the-box, but Quasar is a quite flexible framework, and we can easily achieve "SSR full-stack capabilities" by using a Quasar SSR-middleware :-)

The idea is:

SSR-middleware is an additional layer where all browser-requests are supposed to come through. Therefore, all we need to do is to add an additional SSR-middleware to intercept browser-requests when trying to hit a specific route (in this case, /api/*). Step-by-step:

Disclaimer: it works only when running Quasar in SSR mode, of course.

  1. Considering that we have already created our Quasar application, and have already added the SSR mode (npm init quasar && cd my-app && quasar mode add ssr), let's add an additional SSR-middleware "api" with the command:
quasar new ssrmiddleware api
  1. After that we need to include our SSR-middleware into quasar.config.js:
ssr: {
    ...
    middlewares: [
        'api', // <= this is our SSR-middleware (keep this as first one)
        'render' // keep this as last one
    ]
},
...
  1. And the last step is to implement the browser-requests interception into our SSR-middleware file src-ssr/middlewares/api.ts:
export default ssrMiddleware(async ({ app, resolve }) => {
    app.all(resolve.urlPath('*'), (req, res, next) => {
        if (req.url.substring(0, 4) === '/api') {
            // Here we can implement our backend NodeJS/Express related operations.
            // See the example below "Real-life example", which provides
            // something similar to Next/Nuxt server-directory functionality.
            res.status(200).send(`Hi! req.method: ${req.method}, req.url: ${req.url}`);
        } else {
            next();
        }
    });
});

Now we can start our Quasar SSR application, and see our API in action by pointing the browser to http://localhost:9100/api/foo/bar/ Response => Hi! req.method: GET, req.url: /api/foo/bar/


Real-life example:

updated according to @David's suggestion. Thanks :-)

  1. In order to provide something similar to Next/Nuxt server-directory functionality, we need to include some additional lines of code into our SSR-middleware file src-ssr/middlewares/api.ts. The example below maps all available API-Handler (see apiHandlers object) and select them to handle browser requests according to the request URL:
import { ssrMiddleware } from 'quasar/wrappers';

const apiHandlers: { [key: string]: any } = {
  'req-info': import('../../src/api/req-info'),
  version: import('../../src/api/version'),
};

export default ssrMiddleware(async ({ app, resolve }) => {
  app.all(resolve.urlPath('*'), (req, res, next) => {
    if (req.url.substring(0, 4) === '/api') {
      try {
        const path = req.url.split('?')[0].substring(5);
        const apiHandler = await apiHandlers[path];
        if (apiHandler) {
          await apiHandler.default(req, res);
        } else {
          res.sendStatus(404);
        }
      } catch (error) {
        console.error(error);
        res.sendStatus(500);
      }
    } else {
      next();
    }
  });
});
  1. And after that we just need to create our NodeJS/Express API-Handlers in the folder /src/api/.
  • Example #1: /src/api/version.ts:
import type { Request, Response } from 'express';
export default function (req: Request, res: Response) {
  const version = require('../../package.json').version;
  res.status(200).send(version);
}
  • Example #2: /src/api/req-info.ts:
import type { Request, Response } from 'express';
export default function (req: Request, res: Response) {
  res.setHeader('Content-Type', 'text/html');
  res.status(200).send(`
  <ul>
    <li>req.url: ${req.url}</li>
    <li>req.method: ${req.method}</li>
    <li>req.query: ${JSON.stringify(req.query)}</li>
  </ul>`);
}

I hope it helps, thanks! If so, please vote-up :-)

StackBlitz project: https://stackblitz.com/edit/quasarframework-uocha6

  • Is there a way to send the HTML from example #2 from an HTML file? Trying to `.sendFile` does not work because the html file in `src-ssr` is not getting included in the build (as far as I can tell). – DevJem Jul 03 '23 at 17:16
  • 1
    Hi DevJem! Note that `sendFile` express API imports the file during runtime _(instead of compilation time)_, which means that we need to use file pathnames according to the "Quasar SSR build output" _(or "Quasar src folder", in case of development mode)_. And note also that only files stored in the `./src/public/` directory are automatically copied into the quasar 'Quasar SSR build output' (execute `quasar build -m ssr`, and check the contents of your `./dist/ssr/` folder). I will continue in the comment below (due to the comment length limitation)... – Samer Eberlin Jul 08 '23 at 20:19
  • 1
    Continuing from the comment above (due to the comment length limitation)... So, I think the simplest solution to use `sendFile` API is: 1. place your "file.html" inside the folder `./src/public/` (instead of inside src-ssr). 2. Adjust the "Example #2" to import "file.html" from `/server/` folder (in case of production build) or from `/public/` folder (in case of development mode): I will continue in the comment below (due to the comment length limitation)... – Samer Eberlin Jul 08 '23 at 20:19
  • 1
    line 1: `import type { Request, Response } from 'express';` ... line 2: `const path = require('path');` line 3: `export default function (req: Request, res: Response) {` line 4: `const publicDir = process.env.DEV ? '../../public' : 'client';` line 5: `res.sendFile(path.join(__dirname, publicDir, 'file.html'));` line 6: `}` – Samer Eberlin Jul 08 '23 at 20:20
  • Sorry for the messy code format above... StackOverflow does not support code-blocks in comments :-( Anyway, I hope it helps :-) – Samer Eberlin Jul 08 '23 at 20:38
  • Samer, thank you for your help! With the current configuration of middleware through Quasar I was able to use `const publicDir = process.env.DEV ? 'public' : 'client'` once I moved my html into the public folder. Last piece was setting the express view engines "views" directory to that `publicDir` with `app.set('views', publicDir)` and I was able to reference it simply with `res.render('file.html')`. – DevJem Jul 10 '23 at 19:50
0

I like the approach of Samer Eberlin, however there is a big drawback, it relies on the source folder, since handlers are dynamically imported from the src directory at runtime. This means that the production build dist is not standalone, since the API handlers are not bundled into the production build.

Here is a version that includes the routes in the production build (with TypeScript and ES Modules support).

./src/api/_routes.ts

Routes configuration, keys are the api paths, /ping will be /api/ping, and the values should be function that will resolve to a module import.

const routes = {
    '/ping': () => import('./ping'),
}

export default routes

./src/api/ping.ts

Example api route/handler, should have a default export.

import type { Request, Response } from 'express'

export default function (req: Request, res: Response) {
    res.status(200).send('pong')
}

./src-ssr/middlewares/api.ts

Quasar ssr middleware to intercept and map the request path to the configured routes, will raise 404 when not mapped, 500 on error and otherwise whatever the handler is doing.

import { ssrMiddleware } from 'quasar/wrappers'
import routes from '../../src/api/_routes'
import type { Request, Response } from 'express'
import { parse } from 'url'

export default ssrMiddleware(async ({ app, resolve }) => {
    app.all(resolve.urlPath('*'), async (req: Request, res: Response, next) => {
        const { pathname } = parse(req.url)
        if (!pathname || !pathname.startsWith('/api')) {
            return next()
        }

        const path = pathname.replace(/^\/api/, '')

        if (!Object.keys(routes).includes(path)) {
            res.sendStatus(404).end()
            return
        }

        console.log(`API hit on ${path}`)

        try {
            const handler = (await routes[path as keyof typeof routes]())
                .default
            handler(req, res)
        } catch (error) {
            console.error(error)
            res.sendStatus(500).end()
        }
    })
})
David Wolf
  • 1,400
  • 1
  • 9
  • 18
-1

A different approach might be using Nuxt-Quasar. I'm currently in the process of migrating from NuxtV2/VuetifyV2 to NuxtV3/QuasarV2 and this plugin has been amazing so far. The power of Nuxt with the components of Quasar, it might save you some time.

Jason Landbridge
  • 968
  • 12
  • 17