Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Expressjs router based on Content-Type header

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);
like image 640
CherryNerd Avatar asked Mar 09 '16 16:03

CherryNerd


People also ask

What does Express Router () do?

The express. Router() function is used to create a new router object. This function is used when you want to create a new router object in your program to handle requests.

What is Expres JSON?

json() is a built-in middleware function in Express. This method is used to parse the incoming requests with JSON payloads and is based upon the bodyparser. This method returns the middleware that only parses JSON and only looks at the requests where the content-type header matches the type option.

What does res sendFile do?

The res. sendFile() function basically transfers the file at the given path and it sets the Content-Type response HTTP header field based on the filename extension.

Does Res send end the function?

send doesn't return the function, but does close the connection / end the request.


2 Answers

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.

like image 176
jfriend00 Avatar answered Oct 09 '22 12:10

jfriend00


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.
like image 8
mLuby Avatar answered Oct 09 '22 11:10

mLuby