Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

JavaScript REST APIs with Express Refactoring and Modularizing a REST API Get a Random Quote

Akshay Alok
Akshay Alok
7,857 Points

How would my code identify between /quotes/:id and /quotes/random?

For /quotes/random to work I had put it above /quotes/:id, is there no other way? I want a route to /quotes/:id only when /:id is a number if the url after the /quotes/ is not a number, then I don't want to route to /quote/:id. Can this be done? Or would I have to code some sort of logic inside the route /quotes/:id...

Akshay Alok
Akshay Alok
7,857 Points

I just want the word 'random' from /quotes/random to NOT be appointed as the value to req.params.id in /quotes/:id

I like Ben McMahan's solution by ordering the routes accordingly such that the explicit routes are handled first, and the /quotes/:id route parameter isn't 'short-circuiting' the /quotes/random route. However, if you want to know how to handle it otherwise, here's how I did it. This was my original solution as I thought the challenge involved using /quotes/random route.

// Send a GET request to /quotes/:id READ(view) a quote
router.get('/quotes/:id', asyncHandler ( async (req, res, next) => {
   const id = req.params.id;
   // to redirect it if the url is .../quotes/random, have to RETURN next() so it does not execute the rest of this function
   if (id === "random") return next();
   const quote = await records.getQuote(parseInt(id))
   if (quote) {
      res.json(quote);
   } else {
      res.status(404).json({message : "Quote not found"})
   }
}))


// Send a GET request to /quotes/quote/random to READ(view) a random quote
router.get('/quotes/random', asyncHandler( async (req, res, next) => {
   const theQuotesObj = await records.getQuotes();
   const quotes = theQuotesObj.records;
   const quote = quotes[Math.floor(Math.random() * quotes.length)]
   res.status(200).json(quote);
}))

5 Answers

Brad Chandler
Brad Chandler
15,301 Points

Based on your own comment, you could check if the :id (req.params.id) is a number by trying to coerce it to a number. If it is not, then you could call next(); to pass on to the next middleware and eventually return a 404 error.

router.get('/quotes/:id', (req, res, next) => { if (isNaN(parseInt(req.params.id))) { next(); } else { //perform response stuff here } });

The if statement parses the id as a number, if it fails it will return NaN (not a number) which is caught by isNaN() and returns true if the id is not a number.

I think the answer to your original question is quite simple. You made a mistake in your route. The random route is supposed to be /api/quotes/quote/random.
I was wondering how the /quotes/:id route didn't get mixed up with the /quotes/quote/random route when it seems that /quote/random would fill in as the :id in the first route.
It seems that if there is a "/" within the id and it isn't expected, the router simply moves on to the next middleware.
Try testing out a get request to /api/quotes/quoterandom or /api/quotes/quote and you will see a 404 error if you have a try...catch block set up to catch an empty quote object. Then if you make it /api/quotes/quote/random it will work.

James Crosslin
seal-mask
.a{fill-rule:evenodd;}techdegree seal-36
James Crosslin
Full Stack JavaScript Techdegree Graduate 16,882 Points

I just realized I misunderstood Akshay, so let's handle what they're talking about: making sure a "/quotes/random" request does not get resolved by "/quotes/:id". For this, we just need a simple conditional:

router.get(
  '/quotes/:id',
  asyncHandler(async (req, res) => {
    const { id } = req.params
    if(id === "random") res.redirect('/quotes/random')
    const quote = await records.getQuote(id)
    if (quote) return res.json(quote)
    throw new Error("Could not find quote")
  })
)

And that's all! But honestly, I would just put "/quotes/random" before "/quotes/:id" if you don't want to change the path to "/quotes/quote/random". A request sent to a node server checks the first router listed to the last. We as developers should use that functionality to our advantage. It's not a bug, it's just how it natively works and it allows us to set up proper gateways for our routes. So just pop that "/quotes/random" to the top of your routes.js (or at least above "/quotes/:id") and your problems are solved!

Tyler McDonald
seal-mask
.a{fill-rule:evenodd;}techdegree seal-36
Tyler McDonald
Full Stack JavaScript Techdegree Graduate 16,700 Points

But honestly, I would just put "/quotes/random" before "/quotes/:id" if you don't want to change the path to "/quotes/quote/random". A request sent to a node server checks the first router listed to the last. We as developers should use that functionality to our advantage. It's not a bug, it's just how it natively works and it allows us to set up proper gateways for our routes.

This is the best answer in my opinion. Why add extra code when we could structure the routes in such a way that we don't have to perform conditional checks?

codingchewie
codingchewie
8,764 Points

If I am understanding what you're asking is you want if someone passes in a bad req to automatically generate a random quote? Meaning you want to change:

localhost:3000/api/quotes/dfdfsdfsd

instead of getting a message: 'Quote not found.' you want some random post returned? To do this you can leave /quotes/quote/random and just modify the code for '/quotes/:id'. Instead of returning an error you can always return something:

router.get(
  '/quotes/:id',
  asyncHandler(async (req, res) => {
    let quote

    quote = await records.getQuote(req.params.id)
    if (quote) return res.json(quote)

    quote = await records.getRandomQuote()
    return res.json(quote)
  }),
)
James Crosslin
seal-mask
.a{fill-rule:evenodd;}techdegree seal-36
James Crosslin
Full Stack JavaScript Techdegree Graduate 16,882 Points

codingchewie's answer isn't a bad one, but it is repeated code and infringes on the territory of another route we created. I would suggest adding a redirect like this for the use case they presented.

router.get(
  '/quotes/:id',
  asyncHandler(async (req, res) => {
    const quote = await records.getQuote(req.params.id)
    if (quote) return res.json(quote)
    res.redirect('/quotes/random')
  })
)

Although this would work, I would be more inclined to check the req.params for the string 'random'. to handle my redirect. That way you are not relying on the getQuote function to return a false value for the redirect to happen. If the getQuote() function throws an error for an invalid id, which is what the string 'random' would be, it would be handled by the Express error middleware and would not redirect.

James Crosslin - I'm surprised the res.redirect('/quotes/random') would not cause an infinite loop. I would think the redirected url to '/quotes/random' goes back to being handled by router.get('/quotes/:id') ??

Ben McMahan
Ben McMahan
7,922 Points

Coming from a lot of years writing routes in the Laravel PHP framework, I have always gone with putting all routes with a direct URL above those that expect a parameter. It's easier to read and flows without having to check if a parameter is present or if it matches a type or specific value.

Express routing works the same way. In my solution, the router will first run /quotes/random because it is explicitly defined. Otherwise, it will skip to the next route with a parameter.

// GET request to return a random quote
router.get('/quotes/random', asyncHandler(async (req, res) => {
    const randomQuote = await records.getRandomQuote();
    if (randomQuote) {
        res.json(randomQuote);
    } else {
        res.status(404).json({message: "Sorry, there are no quotes"});
    }
}));

// Send a GET request to /quotes/:id to READ a quote
router.get('/quotes/:id', asyncHandler(async (req, res) => {
    const quote = await records.getQuote(req.params.id);
    if (quote) {
        res.json(quote);
    } else {
        res.status(404).json({ message: "Quote not found" });
    }
}));