10

For routing, I'd like my middleware to pass the request the routes defined in a /html folder to server HTML(ejs), and if header Content-Type is application/json, use the routes defined in the /api folder.

But I don't want to have to define that in every route. So I'm not looking for middleware that defines some req.api property that I can check on in every route

app.get('/', function(req, res) {
    if(req.api_call) {
        // serve api
    } else {
        // serve html
    }
});

But I'd like something like this:

// HTML folder
app.get('/', function(req, res) {
    res.send('hi');
});

// API folder
app.get('/', function(req, res) {
    res.json({message: 'hi'});
});

Is this possible and if so, how can I do this?

I'd like it to work something like this:

app.use(checkApiCall, apiRouter);
app.use(checkHTMLCall, htmlRouter);
CherryNerd
  • 1,258
  • 1
  • 16
  • 38
  • See here about checking the request content type - http://stackoverflow.com/questions/18902293/nodejs-validating-request-type-checking-for-json-or-html – drorw Mar 09 '16 at 16:21
  • I know how to check the request content-type. The problem I'm facing is wanting to use different express Routers based on the Content-Type, instead of just the url like `app.use('/api', apiRouter);` does – CherryNerd Mar 09 '16 at 16:23
  • Since routers inherently work by path, I wonder if you could have some middleware that checks the content type and modifies the path to be a pseudo-path that includes something to represent the content type and then set your routers up to serve the modified path. For example the request for `'/'` with JSON content type could be modified to a pseudo path of `'/api/'` so you'd serve that with `app.get('/api/', ...)`. I'm not entirely sure that routers still work after you modified the path in the request, but you could try it easy enough. – jfriend00 Mar 09 '16 at 16:28
  • It might be not possible without adding those `if` statements within router functions as `app.use()` doesn't seem to implement any additional restrictions possibilities but path. – Nonemoticoner Mar 09 '16 at 16:31
  • @jfriend00 Wasn't path read-only? – Nonemoticoner Mar 09 '16 at 16:33
  • This is possible in middleware to skip a route. `next('route');` shouldn't that also be possible for skipping the current Router or so? – CherryNerd Mar 09 '16 at 16:35
  • @Nonemoticoner - You'd have to either study the expressjs code or try it to see if it could work and it's why my suggestion is only a comment not an answer. It's an idea. – jfriend00 Mar 09 '16 at 16:45
  • Which header are you looking to compare to `application/json` on an incoming request? – jfriend00 Mar 09 '16 at 17:09
  • @Nonemoticoner - I just verified that `req.url` can be modified and it will affect Express routing. So, one can just modify `req.url` into a pseudo URL in the first middleware handler based on the request type. I've put that into an answer below. – jfriend00 Mar 09 '16 at 17:18
  • @jfriend00 Cool! Because there was someone posting question on Stack like few days ago and there was a belief that property is read-only. But it could be some other property storing the url. Can't remember excactly. – Nonemoticoner Mar 09 '16 at 17:35

2 Answers2

12

You can insert as the first middleware in the Express chain, a middleware handler that checks the request type and then modifies the req.url into a pseudo URL by adding a prefix path to it. This modification will then force that request to go to only a specific router (a router set up to handle that specific URL prefix). I've verified this works in Express with the following code:

var express = require('express');
var app = express();
app.listen(80);

var routerAPI = express.Router();
var routerHTML = express.Router();

app.use(function(req, res, next) {
    // check for some condition related to incoming request type and
    // decide how to modify the URL into a pseudo-URL that your routers
    // will handle
    if (checkAPICall(req)) {
        req.url = "/api" + req.url;
    } else if (checkHTMLCall(req)) {
        req.url = "/html" + req.url;
    }
    next();
});

app.use("/api", routerAPI);
app.use("/html", routerHTML);

// this router gets hit if checkAPICall() added `/api` to the front
// of the path
routerAPI.get("/", function(req, res) {
    res.json({status: "ok"});
});

// this router gets hit if checkHTMLCall() added `/api` to the front
// of the path
routerHTML.get("/", function(req, res) {
    res.end("status ok");
});

Note: I did not fill in the code for checkAPICall() or checkHTMLCall() because you were not completely specific about how you wanted those to work. I mocked them up in my own test server to see that the concept works. I assume you can provide the appropriate code for those functions or substitute your own if statement.

Prior Answer

I just verified that you can change req.url in Express middleware so if you have some middleware that modifies the req.url, it will then affect the routing of that request.

// middleware that modifies req.url into a pseudo-URL based on 
// the incoming request type so express routing for the pseudo-URLs
// can be used to distinguish requests made to the same path 
// but with a different request type
app.use(function(req, res, next) {
    // check for some condition related to incoming request type and
    // decide how to modify the URL into a pseudo-URL that your routers
    // will handle
    if (checkAPICall(req)) {
        req.url = "/api" + req.url;
    } else if (checkHTMLCall(req)) {
        req.url = "/html" + req.url;
    }
    next();
});

// this will get requests sent to "/" with our request type that checkAPICall() looks for
app.get("/api/", function(req, res) {
    res.json({status: "ok"});
});

// this will get requests sent to "/" with our request type that checkHTMLCall() looks for
app.get("/html/", function(req, res) {
    res.json({status: "ok"});
});

Older Answer

I was able to successfully put a request callback in front of express like this and see that it was succesfully modifying the incoming URL to then affect express routing like this:

var express = require('express');
var app = express();
var http = require('http');

var server = http.createServer(function(req, res) {
    // test modifying the URL before Express sees it
    // this could be extended to examine the request type and modify the URL accordingly
    req.url = "/api" + req.url;
    return app.apply(this, arguments);
});

server.listen(80);

app.get("/api/", function(req, res) {
    res.json({status: "ok"});
});

app.get("/html/", function(req, res) {
    res.end("status ok");
});

This example (which I tested) just hardwires adding "/api" onto the front of the URL, but you could check the incoming request type yourself and then make the URL modification as appropriate. I have not yet explored whether this could be done entirely within Express.

In this example, when I requested "/", I was given the JSON.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • 1
    Added several more versions to the answer. The latest shows that middleware can modify the URL into a pseudo-path that will then be matched by Express routers. So you can have each set of requests in its own router and one simple middleware function that directs the request to the appropriate router by simply modifying `req.url` into a pseudo-URL. – jfriend00 Mar 09 '16 at 17:38
  • IMHO, I think it's a 'dirty' hack, but it works, so w/e xD Thanks man! I REALLY did the job! Awesome!! – CherryNerd Mar 09 '16 at 21:02
  • 1
    @CreasolDev - It is a bit of a hack, but it's a pretty clean hack as the only thing it relies on is that you can change `req.url` in an early middleware which is used for other reasons too. Beyond that it's straight routing, so it's actually a pretty clean hack as hacks go. – jfriend00 Mar 09 '16 at 21:19
8

To throw my hat in the ring, I wanted easily readable routes without having .json suffixes everywhere.

router.get("/foo", HTML_ACCEPTED, (req, res) => res.send("<html><h1>baz</h1><p>qux</p></html>"))
router.get("/foo", JSON_ACCEPTED, (req, res) => res.json({foo: "bar"}))

Here's how those middlewares work.

function HTML_ACCEPTED (req, res, next) { return req.accepts("html") ? next() : next("route") }
function JSON_ACCEPTED (req, res, next) { return req.accepts("json") ? next() : next("route") }

Personally I think this is quite readable (and therefore maintainable).

$ curl localhost:5000/foo --header "Accept: text/html"
<html><h1>baz</h1><p>qux</p></html>

$ curl localhost:5000/foo --header "Accept: application/json"
{"foo":"bar"}

Notes:

  • I recommend putting the HTML routes before the JSON routes because some browsers will accept HTML or JSON, so they'll get whichever route is listed first. I'd expect API users to be capable of understanding and setting the Accept header, but I wouldn't expect that of browser users, so browsers get preference.
  • The last paragraph in ExpressJS Guide talks about next('route'). In short, next() skips to the next middleware in the same route while next('route') bails out of this route and tries the next one.
  • Here's the reference on req.accepts.
mLuby
  • 673
  • 7
  • 15
  • 1
    The `HTML_ACCEPTED` and JSON_ACCEPTED` functions look clean and feel clean in the `router.get` definition. By this time though, I've decided to go for a "single point of exit" kind of routing, where all different routes just process the data required and pass it all through to 1 last `ResponseRouter` that serves data based on the `Content-Type`. It renders EJS pages for HTML or serves a JSON response for `Content-Type: Application/JSON` But thanks for the response, looks great! – CherryNerd Dec 17 '18 at 20:11