I am developing a react app that uses server-side rendering.
My routes work, except the homepage. Not sure why. I'm assuming I'm not setting up my express server correctly to handle the index route...?
The following is the module which handles express' setup and whatnot. I'm assuming I have the app.get('*) or app.use(express.static) incorrect.
app.js (server)
require('ignore-styles')
const compression = require('compression')
const express = require('express')
const path = require('path')
require('babel-register')({
ignore: /\/(build|node_modules)\//,
presets: ['env', 'react-app']
})
const universalLoader = require('./universal')
const app = express()
// Support Gzip
app.use(compression())
// Serve static assets
app.use(express.static(path.resolve(__dirname, '..', 'build')))
// Always return the main index.html, so react-router render the route in the client
app.get('*', universalLoader)
module.exports = app
universalLoader.js (server)
import path from 'path'
import fs from 'fs'
import React from 'react'
import { Provider } from 'react-redux'
import { renderToString } from 'react-dom/server'
import { StaticRouter, matchPath } from 'react-router-dom'
import configureStore from '../src/store'
import App from '../src/components/App'
import routes from '../src/shared/routes'
import { getSiteInfo } from '../src/store/actions/siteInfo'
import { REACT_APP_SITE_KEY } from '../src/shared/vars'
import Helmet from 'react-helmet'
module.exports = function universalLoader(req, res, next) {
// console.log('Loading....')
const store = configureStore()
const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl
let routeFound = false
// Try to find a matched route
const promises = routes.reduce((promise, route) => {
var props = matchPath(req.url, route)
// If route was matched, component exists, and has an initialAction
// then call it's initialAction.
// This action will most-likely load some data asyncronously
if (props && route.component && route.component.initialAction) {
promise.push(Promise.resolve(store.dispatch(route.component.initialAction(store, props))))
}
return promise
}, [])
// Load initial site data
promises.push(Promise.resolve(store.dispatch(getSiteInfo(REACT_APP_SITE_KEY))))
// Wait until all async data has been loaded
Promise.all(promises)
.then(() => {
// Load index file path
const filePath = path.resolve(__dirname, '..', 'build', 'index.html')
// Read index file
fs.readFile(filePath, 'utf8', (err, htmlData) => {
if(err){
console.error('read err', err)
return res.status(404).end()
}
const preloadedState = store.getState()
// console.log("PreloadedState:", preloadedState)
const context = preloadedState
// console.log(context)
// Note: Pass in serverRequest prop so the App knows the domain it's on for meta tags
const markup = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<App serverRequest={req} serverResponse={res} />
</StaticRouter>
</Provider>
)
const helmet = Helmet.renderStatic()
// Somewhere a `<Redirect>` was rendered
if(context.url){
console.log('Redirected:', context.url)
redirect(301, context.url)
// we're good, send the response
}else{
// Page meta data
const meta = helmet.title.toString() + helmet.meta.toString() + helmet.link.toString()
// Prep state to be injected into DOM for client
const pageState = `<script>window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}</script>`
// Inject state and markup
const RenderedApp = htmlData
.replace('<script></script>', pageState) // Store state to pass to client
.replace('<meta name="helmet">', meta) // Meta data
.replace('{{SSR}}', markup) // Actual markup/component html
console.log("SSR Rendered: ", req.path)
res.send(RenderedApp)
}
})
})
.catch(err => {
console.log("Error:", err)
})
}
I am console.log()-ing when a route is being handled within universalLoader(). All routes show in the console that stuff is happening. Except my homepage. It does not even show the "Loading..." message.
express.static will be serving up any files in your build directory. If it finds the requested file it will serve it up and end the request/response. No middleware registered after express.static will get the chance to run if a suitable file is found.
Based on this line:
const filePath = path.resolve(__dirname, '..', 'build', 'index.html')
it would appear that you have a file called index.html in your build directory. This will get served up by express.static when you hit the URL for index.html but it will also get served up if you just hit the / URL because express.static defaults to serving up index.html. See the index property here:
https://expressjs.com/en/4x/api.html#express.static
The directory you point express.static at needs to contain files that are static, i.e. that require no processing whatsoever. If any of the files need processing they need to live elsewhere. Note the parallels with how an Express app typically has a separate folder for views, which in many ways is similar to what you're trying to do.
I would also suggest commenting out express.static to see what effect that has. It should be a quick way to confirm that express.static is responsible for stopping your index route being reached.
Update
Based on your comments it would seem that you do have a static directory at build/static that contains your JS and CSS files. You can serve this up directly using:
app.use(express.static(path.resolve(__dirname, '..', 'build', 'static')))
However, this will cause all your URLs to change too, so http://localhost/static/js/example.js will now be http://localhost/js/example.js. To retain the original URLs you would need to put the static back in via the route path:
app.use('/static', express.static(path.resolve(__dirname, '..', 'build', 'static')))
Related
I'm able to dynamically render html for server side rendering using angular 8 and angular universal when I view page source BUT my issue is that I am rendering the dynamic html before the api request has finished loading so I can't see the response in the dynamic html from the view page source - please let me know if you require any further info. I run this command:
i.e. npm run build:ssr && npm run serve:ssr
and when I look at the logs and change the url I get a [NetworkError] in the console but the app still runs as expected, I need to figure out a way to load the api response and then render the html into the page view source after the request has finished but I've run out of ideas.
Hopefully one of you guys can help,
Thanks
Server.ts
import 'zone.js/dist/zone-node';
import 'localstorage-polyfill';
import { join } from 'path';
import * as express from 'express';
const compression = require('compression');
const sessionStorage = require('sessionstorage');
const DIST_FOLDER = join(process.cwd(), 'dist');
const domino = require('domino');
const fs = require('fs');
const template = fs.readFileSync('./dist/browser/index.html').toString();
const win = domino.createWindow(template);
const proxy = require('http-proxy-middleware');
const cors = require('cors');
const helmet = require('helmet');
Object.assign(global, domino.impl);
(global as any)['KeyboardEvent'] = domino.impl.Event;
global['window'] = win;
global['Node'] = win.Node;
global['navigator'] = win.navigator;
global['Event'] = win.Event;
global['KeyboardEvent'] = win.Event;
global['MouseEvent'] = win.Event;
global['Event']['prototype'] = win.Event.prototype;
global['document'] = win.document;
global['sessionStorage'] = sessionStorage;
global['localStorage'] = localStorage;
// Express server
const app = express();
const PORT = process.env.PORT || 4200;
const {AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap} = require('./dist/server/main');
app.use(cors());
app.use(compression());
// express-engine
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
// Protect website from Clickjacking attack
app.use(helmet.frameguard());
app.use(helmet.xssFilter());
// Proxy API Endpoints
app.use('/api/profileProxy', proxy(
{
target: 'http://xxxxxxx1:9004', // target host
changeOrigin: true, // needed for virtual hosted sites
// ws: true, // proxy websockets
pathRewrite: {
'^/api/profileProxy': ''
}
}
));
app.use('/api/searchProxy', proxy(
{
target: 'http://xxxxxx.160:9005', // target host
changeOrigin: true, // needed for virtual hosted sites
// ws: true, // proxy websockets
pathRewrite: {
'^/api/searchProxy': ''
}
}
));
app.get('/sitemap1.xml', function (req, res, next) {
const file = `${DIST_FOLDER}/sitemap1.xml`;
fs.exists(file, function (exists) {
if (exists) {
res.sendFile(file);
} else {
res.status(404).send('404');
}
});
});
app.get('/robots.txt', function (req, res, next) {
const file = `${DIST_FOLDER}/robots.txt`;
fs.exists(file, function (exists) {
if (exists) {
res.sendFile(file);
} else {
res.status(404).send('404');
}
});
});
// All regular routes use the Universal engine
app.get('*', (req, res) => {
console.time(`GET: ${req.originalUrl}`);
console.log(`req-QQQQQQQQQQ: ${req.originalUrl}`);
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
console.timeEnd(`GET: ${req.originalUrl}`);
console.log(`req-timeEnd: ${req.originalUrl}`);
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`);
});
``
What you are describing is normal behaviour. Only your original request (e.h. when you type in the site's adress) in the URL bar will be prerendered using angular universal. This prerendered html is what you can see if you use View Source (or Ctrl +U)
Once the page is displayed, the client side angular app takes over. When you click on the search button, it'll perform an API call and get results, which will update the page. You'll see that html using the Html inspector from your browser's debugging tool, but it will have no impact on the original HTML that has been sent by the server
for people that may come across this issue - I was able to display the data from the api request (in my case the initial search results) in the view page source for SSR by making a GET request in the app.component of my app - thanks
I'm using indexeddb in an Angular 8 service and need window. The code builds without errors and the app creates the db objectstore flawlessly. But at runtime in production mode (with an actual node server instead of ng serve where this error does not occur), I get this error in the terminal running angular:
ERROR ReferenceError: window is not defined
at IndexedDBService.isSupported (D:\MartijnFiles\Documents\Programming\Fenego\fenego-labs-angular\dist\server.js:71199:9)
at IndexedDBService.openDB (D:\MartijnFiles\Documents\Programming\Fenego\fenego-labs-angular\dist\server.js:71203:18)
at Promise (D:\MartijnFiles\Documents\Programming\Fenego\fenego-labs-angular\dist\server.js:72026:46)
Again, it all works and the isSupported() function would stop openDB() from being run if window was actually undefined. There is also no error in the browser console.
Here is the relevant part of my service.
#Injectable()
export class IndexedDBService {
isSupported(): boolean {
return !!window.indexedDB;
}
openDB(dbName: string,
version: number,
onUpgradeNeededCallback: OnUpgradeNeededCallback,
onSuccessCallback: OnOpenSuccessCallback,
onErrorCallback: OnOpenErrorCallback,
onBlockedCallback: OnOpenBlockedCallback): Observable<IDBOpenDBRequest> {
let openDBRequest: IDBOpenDBRequest = null;
if (this.isSupported()) {
openDBRequest = window.indexedDB.open(dbName, version);
openDBRequest.onupgradeneeded = onUpgradeNeededCallback;
openDBRequest.onsuccess = onSuccessCallback;
openDBRequest.onerror = onErrorCallback;
openDBRequest.onblocked = onBlockedCallback;
}
return of(openDBRequest);
}
There are many suggest "solutions" out there that mostly boil down to providing it via a service or plain injection (eg. point 1 in this blog https://willtaylor.blog/angular-universal-gotchas/) but all it does is pass window from some other service via injection to mine. But my code works so it clearly has access to window...
Update:
The following line in a component's ngOnInit() has the same problem with Worker being "not defined" yet the worker is loaded and runs perfectly:
const offlineProductsWorker = new Worker('webworkers/offline-products-worker.js');
Update2:
I have found a solution (posted below) but checking for server side rendering seems more like a workaround than solving the fact that server side rendering is happening (not sure if that is supposed to be the case).
I will include my server.ts script that I use with webpack below. It is a modification of one from another project and I don't understand most of it. If anyone can point out to me what I could change to stop the server side rendering, that would be great. Or, if it is supposed to do that then why?
// tslint:disable:ish-ordered-imports no-console
import 'reflect-metadata';
import 'zone.js/dist/zone-node';
import { enableProdMode } from '#angular/core';
import * as express from 'express';
import { join } from 'path';
import * as https from 'https';
import * as fs from 'fs';
/*
* Load config from .env file
*/
require('dotenv').config({ path: './ng-exp/.env' });
const IS_HTTPS = process.env.IS_HTTPS === 'true';
const SSL_PATH = process.env.SSL_PATH;
const ENVIRONMENT = process.env.ENVIRONMENT;
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
const logging = !!process.env.LOGGING;
// Express server
const app = express();
const PORT = process.env.PORT || 4200;
const DIST_FOLDER = process.cwd();
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');
// Express Engine
import { ngExpressEngine } from '#nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '#nguniversal/module-map-ngfactory-loader';
// Our Universal express-engine (found # https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine(
'html',
ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)],
})
);
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'ng-exp'));
// Server static files from /browser
app.get(
'*.*',
express.static(join(DIST_FOLDER, 'ng-exp'), {
setHeaders: (res, path) => {
if (/\.[0-9a-f]{20,}\./.test(path)) {
// file was output-hashed -> 1y
res.set('Cache-Control', 'public, max-age=31557600');
} else {
// file should be re-checked more frequently -> 5m
res.set('Cache-Control', 'public, max-age=300');
}
},
})
);
// ALl regular routes use the Universal engine
app.get('*', (req: express.Request, res: express.Response) => {
if (logging) {
console.log(`GET ${req.url}`);
}
res.render(
'index',
{
req,
res,
},
(err: Error, html: string) => {
res.status(html ? res.statusCode : 500).send(html || err.message);
if (logging) {
console.log(`RES ${res.statusCode} ${req.url}`);
if (err) {
console.log(err);
}
}
}
);
});
const sslOptions = {
key: fs.readFileSync(`${SSL_PATH}/${ENVIRONMENT}/server.key`),
cert: fs.readFileSync(`${SSL_PATH}/${ENVIRONMENT}/server.crt`),
};
// Start up the Node server
let server;
if (IS_HTTPS) {
server = https.createServer(sslOptions, app);
} else {
server = app;
}
server.listen(PORT, () => {
console.log(`Node Express server listening on http${IS_HTTPS ? 's' : ''}://localhost:${PORT}`);
const icmBaseUrl = process.env.ICM_BASE_URL;
if (icmBaseUrl) {
console.log('ICM_BASE_URL is', icmBaseUrl);
}
});
There is a related issue here:
https://github.com/hellosign/hellosign-embedded/issues/107
Basically, to avoid the error you can declare somewhere globally the window.
if (typeof window === 'undefined') {
global.window = {}
}
I also found React JS Server side issue - window not found which explains the issue better and why it works on the client side.
I found the solution thanks to some input from ChrisY
I deploy my code using webpack and run it using node. It seems that node somehow renders it server side and then the browser renders it too. The server site portion has no effect on the storefront but does cause the (seemingly harmless) error. In isSupported() I added console.log(isPlatformBrowser(this.platformId))and it printed false in the server terminal but true in the browser. Thus, I changed the code as follows:
constructor(#Inject(PLATFORM_ID) private platformId: any) {}
isSupported(): boolean {
return isPlatformBrowser(this.platformId) && !!indexedDB;
}
Now it still works in the browser as it did before but there is no server error.
Update:
I have also found the cause for the server side rendering. The server.ts file in the description has a block with res.render(. This first renders the page on the server and if it does not receive html, it returns status code 500. Otherwise it allows the client to render it. Seeing as this is a realistic scenario, I have decided to keep the extra isPlatformBrowser(this.platformId) check in my code. This should then be done for anything that can only be performed by the client (window, dom, workers, etc.).
Not not have server side rendering, an alternative to the res.render( block is
res.status(200).sendFile(`/`, {root: join(DIST_FOLDER, 'ng-exp')});
I try to get the :userId "albert" from this url
http://localhost:5000/search/albert?query=al&page=1
at server side but failed, what can I do to get the react-router defined params correctly at node.js with express?
routes.js
[
{
path: '/search/:userId',
component: Search,
}, {
path: '/search',
component: Search,
}
...
]
server.js
server.get('*', async (req, res, next) => {
const pageData = await routes
.filter(route => matchPath(req.path, route))
.map((route) => {
console.log(route)
return route.component
})
}
The React-Router Way
React Router V4 does include a way to extract param data server-side using their matchPath() function, using their standard parameter implementation, "/path-name/:param" route matching.
In this case, it allows me to do a lot of server-side stuff based on the parameter before the express app responds with the page data.
NOTE: this is probably not the most basic implementation, but it's a pared down version of my complete SSR react implementation that makes use of matchPath().
Requirements
Server-side rendered react app
React-router-dom v4
Centralized routes file (because SSR)
Express app server (I'm hosting my express app on Firebase)
In This Example, a server-side express app attempts to run an "initialAction" function in each component during a fresh page load. It passes promise resolve and reject to know when the function is completed running, and the request object which may contain useful params we can extract with matchPath(). It does this for every matching route, again, using matchPath().
Routes.js Example
Where :id is the "id" param in the URL.
const routes = [
{
path: "/news-feed/:id",
component: NewsFeed,
exact: true
},
]
export default routes;
Component Example
Just showing the initialAction() function in the component
import { Link, matchPath } from 'react-router-dom';
class NewsFeed extends Component {
// Server always passes ability to resolve, reject in the initial action
// for async data requirements. req object always passed from express to
// the initial action.
static initialAction(resolve, reject, req) {
function getRouteData() {
let matchingRoute = routes.find(route => {
return matchPath(req.path, route);
});
console.log("Matching Route: ", matchingRoute);
return matchPath(req.path, matchingRoute);
}
let routeData = getRouteData();
console.log("Route Data: ", routeData);
}
/** REST OF COMPONENT **/
Console.log output for the url www.example.com/news-feed/test would be
Route Data: { path: '/news-feed/:id',
url: '/news-feed/test',
isExact: true,
params: { id: 'test' } }
As you can see, we've found our param on the server-side using no regex. matchPath() did the work for us. We can use nice, clean urls.
Server-side index.js
Where the initial action is called, with the promise resolve, reject, and req objects. Keep in mind this is a firebase hosting example and may differ for different hosting providers - your method for the initialAction function call may also differ.
import React from "react";
import ReactDOMServer from 'react-dom/server';
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { StaticRouter, matchPath } from "react-router-dom";
import routes from "../shared/components/App/routes.js";
import express from "express";
import * as functions from "firebase-functions";
// Import Components, Reducers, Styles
import App from "../shared/components/App";
import reducers from "../shared/reducers";
// Prepare our store to be enhanced with middleware
const middleware = [thunk];
const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore);
// Create store, compatible with REDUX_DEVTOOLS (chrome extension)
const store = createStoreWithMiddleware(reducers);
// Implement cors middleware to allow cross-origin
const cors = require('cors')({ origin: true });
const app = express();
app.get('**', (req, res) => {
cors(req, res, () => {
// Finds the component for the given route, runs the "initial action" on the component
// The initialAction is a function on all server-side renderable components that must retrieve data before sending the http response
// Initial action always requires (resolve, reject, req), and returns a promise.
const promises = routes.reduce((acc, route) => {
if (matchPath(req.url, route) && route.component && route.component.initialAction) {
acc.push(new Promise(function (resolve, reject) {
// console.log("Calling initial action...");
store.dispatch(route.component.initialAction(resolve, reject, req));
}));
}
return acc;
}, []);
// Send our response only once all promises (from all components included in the route) have resolved
Promise.all(promises)
.then(() => {
const context = {};
const html = ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
</Provider>
);
const preloadedState = store.getState();
res.status(200).send(renderFullPage(html, preloadedState));
})
.catch(function (error) {
console.log("Promise error at server", error);
});
});
});
module.exports = functions.https.onRequest(app);
Just used a sample node.js app to make a server.js which could be like
const express = require('express')
const app = express()
app.get('/search/:userid', (req, res) => res.json({ key: `Hello World for search with id=${req.params.userid}` }))
app.get('/search', (req, res) => res.send('Hello World!i for search'))
app.get('*', (req, res) => res.send('Hello World!'))
app.listen(3000, () => console.log('Example app listening on port 3000!'))
For the page number and other url params you can do like
req.query['page']
to retrieve the parameters.
Goal: Modify Open Graph meta tags when web crawlers visit different routes.
I know Angular2 4.0.0 has a MetaService, and there is always jQuery, but web crawlers except Googlebot don't execute Javascript, so it is kind of useless to my purpose. While Angular Universal sounds like an overkill for changing a couple meta tags.
So far my solution is to copy and paste the compiled index.html in /dist to index.ejs, and modify the necessary fields. Is it possible to integrate the workflow with the angular-cli compilation process, by having the entry point changed from index.html to index.ejs? If not, what are the alternatives that I should explore?
In my index.ejs :
<meta property="og:url" content="<%= url %>" />
In my Express route index.js :
res.render('index', {
url: site_url,
});
In my server.js:
app.set('views', path.join(__dirname, '/dist'));
app.set('view engine', 'ejs');
Please refrain your answer to the current #angular/cli version (v1.0.1 compatible).
Some related discussions:
Add customization options for HtmlWebpackPlugin #3338
Conditional template logic in index.html for beta.11-webpack #1544
Provide config to rename index.html in dist folder #2241
I was able to solve this by using Nunjucks to render templates served via Angular Universal. I'm sure it's possible to use other engines such as EJS as well. Here are the relevant portions of my server.ts:
import * as dotenv from 'dotenv';
dotenv.config();
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import * as nunjucks from 'nunjucks';
import { renderModuleFactory } from '#angular/platform-server';
import { enableProdMode } from '#angular/core';
import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const app = express();
const PORT = process.env.PORT || 4201;
const DIST_FOLDER = join(process.cwd(), 'dist');
// Our index.html we'll use as our template
const template = readFileSync(join(DIST_FOLDER, 'index.html')).toString();
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist-server/main.bundle');
const { provideModuleMap } = require('#nguniversal/module-map-ngfactory-loader');
nunjucks.configure(DIST_FOLDER, {
autoescape: true,
express: app,
});
app.engine('html', (_, options, callback) => {
renderModuleFactory(AppServerModuleNgFactory, {
// Our index.html
document: template,
url: options.req.url,
// DI so that we can get lazy-loading to work differently (since we need it to just instantly render it)
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
}).then(html => {
callback(null, html);
});
});
app.set('view engine', 'html');
// Server static files from dist folder
app.get('*.*', express.static(DIST_FOLDER));
// All regular routes use the Universal engine
// You can pass in server-side values here to have Nunjucks render them
app.get('*', (req, res) => {
res.render('index', { req, yourVar: 'some-value' });
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
I am using express.js as a webserver and would like an easy way to separate all the "app.get" and "app.post" functions to separate files. For example, if I would like to specify get and post functions for a login page, I would like to have a login.js file in a routes folder that is dynamically loaded (will automatically add all of the files without having to specify each one) when I run node app.js
I have tried this this solution!, but it isn't working for me.
app.js
var express=require("express");
var app=express();
var fs=require("fs");
var routePath="./routers/"; //add one folder then put your route files there my router folder name is routers
fs.readdirSync(routePath).forEach(function(file) {
var route=routePath+file;
require(route)(app);
});
app.listen(9123);
I have put below two routers in that folder
route1.js
module.exports=function(app){
app.get('/',function(req,res){
res.send('/ called successfully...');
});
}
route2.js
module.exports=function(app){
app.get('/upload',function(req,res){
res.send('/upload called successfully...');
});
}
Typescript
routes/testroute.ts
import { Router } from 'express';
const router = Router();
router.get('/test',() => {
// Do your stuffs Here
});
export = router;
index.ts
let app = express()
const routePath = path.join(__dirname, 'routes');
fs.readdirSync(routePath).forEach(async (filename) => {
let route = path.join(routePath, filename);
try {
const item = await import(route);
app.use('/api', item.default);
} catch (error) {
console.log(error.message);
}
});
app.listen()
I ended up using a recursive approach to keep the code readable and asynchronous:
// routes
processRoutePath(__dirname + "/routes");
function processRoutePath(route_path) {
fs.readdirSync(route_path).forEach(function(file) {
var filepath = route_path + '/' + file;
fs.stat(filepath, function(err,stat) {
if (stat.isDirectory()) {
processRoutePath(filepath);
} else {
console.info('Loading route: ' + filepath);
require(filepath)(app, passport);
}
});
});
}
This could be made more robust by checking fro correct file extensions etc, but I keep my routes folder clean and did not want the added complexity
With this approach, there is no need to write routes manually. Just setup a directory structure like the URL paths. Example route is at /routes/user/table/table.get.js and API route will be /user/table.
import app from './app'
import fs from 'fs-readdir-recursive'
import each from 'lodash/each'
import nth from 'lodash/nth'
import join from 'lodash/join'
import initial from 'lodash/initial'
const routes = fs(`${__dirname}/routes`)
each(routes, route => {
let paths = route.split('/')
// An entity has several HTTP verbs
let entity = `/api/${join(initial(paths), '/')}`
// The action contains a HTTP verb
let action = nth(paths, -1)
// Remove the last element to correctly apply action
paths.pop()
action = `./routes/${join(paths, '/')}/${action.slice(0, -3)}`
app.use(entity, require(action))
})
Example route:
import { Router } from 'express'
import Table from '#models/table.model'
const routes = Router()
routes.get('/', (req, res, next) => {
Table
.find({user: userIdentifier})
.select('-user')
.lean()
.then(table => res.json(table))
.catch(error => next(error))
})
module.exports = routes