Skip to content

Instantly share code, notes, and snippets.

@ryanoglesby08
Last active April 4, 2024 13:25
Show Gist options
  • Star 70 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • Save ryanoglesby08/1e1f49d87ae8ab2cabf45623fc36a7fe to your computer and use it in GitHub Desktop.
Save ryanoglesby08/1e1f49d87ae8ab2cabf45623fc36a7fe to your computer and use it in GitHub Desktop.
A node.js SPA server that serves static files and an index.html file for all other routes.
/*
Incredibly simple Node.js and Express application server for serving static assets.
DON'T USE THIS IN PRODUCTION!
It is meant for learning purposes only. This server is not optimized for performance,
and is missing key features such as error pages, compression, and caching.
For production, I recommend using an application framework that supports server-side rendering,
such as Next.js. https://nextjs.org
Or, if you do indeed want a Single Page App, the Create React App deployment docs contain a lot
of hosting options. https://create-react-app.dev/docs/deployment/
*/
const express = require('express');
const path = require('path');
const port = process.env.PORT || 8080;
const app = express();
// serve static assets normally
app.use(express.static(__dirname + '/dist'));
// handle every other route with index.html, which will contain
// a script tag to your application's JavaScript file(s).
app.get('*', function (request, response) {
response.sendFile(path.resolve(__dirname, 'index.html'));
});
app.listen(port);
console.log("server started on port " + port);
@shanmugam-sekar
Copy link

Thanks a lot .

@isthisstackoverflow
Copy link

Fun fact: When I link the bundle as relative path "./bundle.js", it won't work on example.com/oh/the/horror due to obvious reasons.

Just posting this for the next time I wonder what I'm doing wrong and find this page. Thanks for snippet!

@blindkrone
Copy link

how do you route the 404s in this one?

@ryanoglesby08
Copy link
Author

ryanoglesby08 commented Feb 1, 2020

how do you route the 404s in this one?

In this example, all routing is done client side. Because of the app.get('*', ...), there really isn't a concept of a 404 to the server. Any URL will return the index.html page.

You can handle unknown URLs however makes most sense with your app and JS framework. Here's an example from the current React Router docs: https://reacttraining.com/react-router/web/example/no-match

If you are asking about 404s of the static assets, such as a request for a /foobar.js (doesn't exist on your server), check the latest docs for express.static for more info: https://expressjs.com/en/4x/api.html#express.static

@hasinthaK
Copy link

Thanks for the gist, very helpful, but i should point out that this server does not handle Invalid urls, I get no such file or directory error when any gibberish path is entered in the url, or am I doing something wrong? I pasted this code exactly as it is, thus it is giving the error,
Anyone have a thought?

@MaxmaxmaximusGitHub
Copy link

Thanks you =)

@etfrer-yi
Copy link

I am just wondering: how is this an SPA? Forgive my ignorance, but it seems like whenever the user goes to another route, the page has to refresh in order to send the file through response.sendFile(path.resolve(__dirname, 'index.html'));, no?

@ryanoglesby08
Copy link
Author

I am just wondering: how is this an SPA? Forgive my ignorance, but it seems like whenever the user goes to another route, the page has to refresh in order to send the file through response.sendFile(path.resolve(__dirname, 'index.html'));, no?

Thanks for your question! There are 2 key things.

First, notice the only defined route is app.get('*', which will match any URL being handled by this server. So, whether the user types http://www.my-spa.com, http://www.my-spa.com/products, or http://www.my-spa.com/products/12345/reviews, the server will always respond with a "single page", the index.html file.

Secondly, when you implement a SPA, you will typically use a client side router, such as React Router. A client side router works by hooking into the browser's URL and history, so that when the user does any navigation, the URL will change but no request will be made to the server, because the client-side router will show the appropriate content for that URL. Only if the user manually clicks the browser's refresh button (or equivalent action) will another request be made to the server.

I hope that helps shed some light on the subject!

I wrote a blog post on this a few years back if you're looking for more info. It's a little old but I think still relevant. http://ryanogles.by/conceptualizing-how-a-modern-single-page-app-is-served/

@etfrer-yi
Copy link

etfrer-yi commented Feb 6, 2021

Wow, I'm still processing all this, but thank you so much for such a detailed answer! I'm very new to web development, hence the confusion and question. I'll definitively be looking into your blog for more haha.

@zakcodez
Copy link

zakcodez commented Sep 23, 2021

You should add static file serving and a simple Cannot <http method> <pathname> as 404

@cloudcompute
Copy link

cloudcompute commented Jan 1, 2022

@ryanoglesby08
Article is very informative. Thanks.

@hasinthaK

Thanks for the gist, very helpful, but i should point out that this server does not handle Invalid urls, I get no such file or directory error when any gibberish path is entered in the url, or am I doing something wrong? I pasted this code exactly as it is, thus it is giving the error, Anyone have a thought?

It is because you are "typing" this in the URL bar and when one does this, the server is contacted straight way and as a consequence, the react app and its react-router is not invoked. There has been a lot of discussion going on about this topic here. Developers have shared the solutions to this problem too.

Hope this helps.

@ryanoglesby08
Copy link
Author

@hasinthaK I'd like to provide an answer to your question, and @Ramandhingra a clarification on your answer.

but i should point out that this server does not handle Invalid urls, I get no such file or directory error when any gibberish path is entered in the url, or am I doing something wrong?

@hasinthaK You probably don't have the index.html file in the correct location.

This server.js file has 3 requirements in order to produce a fully functioning SPA.

  1. There must be an index.html file in the same directory as the server.js file. See line 20, path.resolve(__dirname, 'index.html'). You can change that line if you want to put the index.html file elsewhere.
  2. There must be a "dist" directory containing all static assets, such as images, CSS stylesheets, and JavaScript files. See line 15, express.static(__dirname + '/dist'). Again, you can change this folder name if you want.
  3. The index.html file must reference all static assets using absolute paths, such as <script type="text/javascript" src="/app.js"></script>.

With this setup, all requests for any URL, such as localhost:8080/foo/bar will return the index.html file. And all requests for a static asset, such as localhost:8080/app.js will attempt to return the corresponding file using Express's static files middleware.

It is because you are "typing" this in the URL bar and when one does this, the server is contacted straight way and as a consequence, the react app and its react-router is not invoked.

While this statement is true, it does not address the problem @hasinthaK was having. To clarify, every request to the server will return the index.html file, which is intentional. Then, when your SPA boots on the client, your client-side router can handle the URL to render the appropriate components.

@cloudcompute
Copy link

@ryanoglesby08

I got you. I have read @hasinthaK question again. You are right.. he has problem imitating your code on his machine.

However, whatever I wrote in my previous post, I was talking about another scenario when the application is running in production mode. In this case, the development server is no longer there.

Consider that I have a React component named NotFound which displays the "404.. Page Not Found etc. etc." using some css. Now a user enters the URL "www.my-site.com/foo/bar", it hits the server with the request: "GET /foo/bar"

At the server end, there exists a wildcard rule in this code:

app.get('*', function (request, response) {
  response.sendFile(path.resolve(__dirname, 'index.html'));
});

This app.get() catches the "GET /foo/bar" request. and returns the index.html. While the user was expecting "Page Not Found etc. etc." (with the same css that is is there in the React NotFound component.

How to address this issue what I am looking for.. Ideally speaking, the text "Page Not Found" and its css should be coded only once.

@cloudcompute
Copy link

@ryanoglesby08

I need another clarification. Could you pl. answer it?

When a user clicks on this link, /updateUserProfile <>, in our React SPA application, the application actually makes two network calls
a. POST /updateUserProfile to the API server. This network call goes through the fetch / axios call that we make explicitly from our React code.

b. After clicking the link, the browser's URL changes and as a result, the browser makes an "implicit" GET request to the server which in returns index.html.

Am I right or not? Kindly guide.

Thanks

@ryanoglesby08
Copy link
Author

ryanoglesby08 commented Jan 3, 2022

@Ramandhingra

This app.get() catches the "GET /foo/bar" request. and returns the index.html. While the user was expecting "Page Not Found etc. etc." (with the same css that is is there in the React NotFound component.

How to address this issue what I am looking for.. Ideally speaking, the text "Page Not Found" and its css should be coded only once.

This simple Node.js server does not have any knowledge of your app or it's routes, so ALL LOGIC, including routing and 404 pages, must be handled by the client-side code. This is one of the downsides to client-side rendering, since the server will always respond to any request with a 200 OK HTTP status code.

In this case, you must use client-side routing, such as React Router to handle all routing behavior. For example:

import { BrowserRouter, Switch, Route } from "react-router-dom";

<BrowserRouter>
  <Switch>
    <Route path="/users">
      <Users />
    </Route>
    <Route path="*">
      <NotFound />
    </Route>
  </Switch>
</BrowserRouter>

Now, if the user enters the URL "www.my-site.com/foo/bar", the server will respond with the index.html. Then the index.html will load the JavaScript assets. Then the React code will execute in the browser. Then the React Router Route components will match the URL, causing the <Route path="*"> component and it's child <NotFound /> component to render, and the user will see the "Page Not Found" message.

Here's the docs for React Router's "No Match (404)" support: https://v5.reactrouter.com/web/example/no-match

If you want the server to be aware of your routes, I highly recommend using a server-side rendering (SSR) framework such as Next.js or Remix.

@ryanoglesby08
Copy link
Author

ryanoglesby08 commented Jan 3, 2022

@Ramandhingra

When a user clicks on this link, /updateUserProfile <>, in our React SPA application, the application actually makes two network calls

a. POST /updateUserProfile to the API server. This network call goes through the fetch / axios call that we make explicitly from our React code.

b. After clicking the link, the browser's URL changes and as a result, the browser makes an "implicit" GET request to the server which in returns index.html.

Based on the name of that path "/updateUserProfile" it sounds like this is an action and not a navigation, so an HTML form with an onSubmit handler would probably be a more appropriate and semantic HTML element to use. This also has the benefit of not causing a navigation event by default. You could then navigate the user to a new page after the fetch/axios request if desired.

If you are going full client-side rendered SPA, most (likely all) actions and navigations should not cause another request to the server, since all the code is already on the client and refreshing the page would be wasteful. In the scenario you describe, I imagine you probably want to use an HTML form to trigger the network request, and React Router for any navigation.

import { useHistory } from "react-router-dom";

function UpdateProfile() {
  let history = useHistory();
  let onSubmit = async (event) => {
    event.preventDefault();
    
    // This URL would be handled be some backend server/API. There are no components or views associated with it.
    await axios.post("/updateProfile");

    // Do a client-side navigation using React Router after the network request. This will not cause a request to a server.
    history.push("/home");
  }

  return (
    <form onSubmit={onSubmit}>
      {/* Form fields  */}

      <button type="submit">Update</button>
    </form>
  );
}

@cloudcompute
Copy link

@ryanoglesby08

Wonderful answers. Helped me learn a lot. Thank you so much.

2 more Qs, just need confirmation from you..

a. A GraphQL request can be sent to the Apollo GraphQL server using either HTTP GET or POST. When your backend code has a GraphQL API endpoint that is served using HTTP Get, the request, let's say, /graphql/getUsers will also land up to the app.get("*", ...). Instead it should invoke the route handler.. app.Get("graphql/getUsers", ..)

Therefore, in this case, we need to modify the app.get("", ..) so that it should serve index.html for all routes except those that begin with /graphql. To exclude these /graphql routes, we can use a regular expression instead of the asterisk ().

Am I right?

b. The following line of code serves both a. index.html and b. other static assets like bundle.js, etc.

app.use(express.static(path.join(__dirname, 'dist')));

I think it is better if we do not let the above code to serve index.html using this code:

var options = { index: false }
app.use(express.static(path.join(__dirname, 'dist'), options));     // now serves files like bundle.js, bundle.css, *.png, only

Let the route handler app.get("*", ...) alone serve index.html.

Does this make sense?

@ryanoglesby08
Copy link
Author

@Ramandhingra

You're welcome, glad I can help you out.

re: GraphQL routes

If you want to add additional routing to this server, you could add additional router methods before the catch all.

I recommend following the Express docs on this one. https://expressjs.com/en/4x/api.html#router

// handle static assets
app.use(express.static(path.join(__dirname, 'dist')));

// handle graphql requests
app.get('/graphql', function (request, response) {
  // do stuff...
});

// handle everything else
app.get('*', function (request, response) {
  response.sendFile(path.resolve(__dirname, 'index.html'));
});

Let the route handler app.get("*", ...) alone serve index.html.

express.static only looks at the dist directory, so it is not handling the index.html file. If it is, then nested routes such as www.my-site.com/foo/bar will break unless you also change the path.resolve(__dirname, 'index.html') line to also point to dist. This is why I recommend the index.html file be outside of dist.

I think with the current setup, the express.static middleware will attempt to handle a request for the index.html file, but since the fallthrough option is true by default, it won't find it and will fallthrough to the next middleware and will eventually get handled by the catch all route.

You can certainly add that index: false option, but if your index.html file is outside of the dist directory you don't really need it.

@cloudcompute
Copy link

cloudcompute commented Jan 4, 2022

@ryanoglesby08

Thank you for the quick response.

When CRA (create-react-app) creates a production build, it keeps the generated index.html inside the build folder.

A. CRA creates a production build having the following contents.

Taken from: https://create-react-app.dev/docs/production-build/
npm run build creates a build directory with a production build of your app. Inside the build/static directory will be your JavaScript and CSS files.

The index.html lies in the build folder itself.

..

B. express.static serves all kind of files including html, css, jpg, css residing in the public folder and its sub-folders.

Taken from: https://expressjs.com/en/starter/static-files.html

app.use(express.static('public'))

Now, you can load the files that are in the public directory:
http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/hello.html

..

C. Don't allow express,static to serve index.html

Taken from: https://stackoverflow.com/questions/48056038/express-static-serves-index-html-how-to-deny-it

var options = {
  index: false
}
app.use(express.static(path.join(__dirname, 'dist'), options));

Final Answer based on these 3 points and your inputs:
Our index.html file is "not" outside of the dist directory.

const pathToBuildFolder = path.join(__dirname, "../../web/build");

// Serve all except index.html
const options = {
  index: false
}
app.use(express.static(pathToBuildFolder, options));

// Serve index.html
const indexFile = path.join(pathToBuildFolder, "index.html");
app.get('*', function (request, response) {
  response.sendFile(indexFile);
});

Is this okay with you?

In this line of code, taken from code above, do we need 'join' 'or 'resolve'?

const indexFile = path.join(pathToBuildFolder, "index.html");

@ryanoglesby08
Copy link
Author

Sure, all that is fine. Again, the server in this gist is meant as a short example to illustrate a general point, it is not intended to catch all edge cases or be production ready. Feel free to copy it and modify it as you see fit.

Since you reference CRA, you may just want to use the example they give, which should match your setup: https://create-react-app.dev/docs/deployment/#other-solutions

@cloudcompute
Copy link

cloudcompute commented Jan 5, 2022

Thanks for the confirmation. Yes, I can understand your example is just for illustration.

Somewhere you said, in production you should use a framework like Next.js. Are you not in favour of SPAs? I believe that the SPA technique comes really handy for building apps that are going to be used by a handful of users like Enterpise CRM, ERP, etc. What SPAs lack are SSR and SEO but there are workarounds for these two as of now. I believe that the genius would be able to overcome these two limitations completely in future.

One more question pl.

A user browses to the my-site.com for the first time:

  1. The static assets (excluding index.html) are served first to the browser because the Express server invokes the app.use(express.static..) global middleware first before serving index.html.

  2. Then app.get ("*", ...) route is hit next which sends the index.html to the browser.

  3. The browser reads the index.html and when it comes across the script tags that contain pointers to to the JS and CSS files, it comes back to the server. The server serves it using the global middleware mentioned in Step 1.

Is this how the process works? If yes, it means one unnecessary call to the server.

I think the browser must be smart enough to pick the JS and CSS assets from the user's machine itself which were served in step 1 and the step 3 won't be executed.

Your viewpoints pl.

With Regards

@ryanoglesby08
Copy link
Author

Somewhere you said, in production you should use a framework like Next.js. Are you not in favour of SPAs? I believe that the SPA technique comes really handy for building apps that are going to be used by a handful of users like Enterpise CRM, ERP, etc. What SPAs lack are SSR and SEO but there are workarounds for these two as of now. I believe that the genius would be able to overcome these two limitations completely in future.

I think fully client-side rendered apps have their place, but SSR frameworks are generally a better choice because they allow for more flexibility. For example, with Next.js you can do client-side rendering, server-side rendering, or static generation on a page by page basis. With CRA, you are limited to only client-side rendering. One of the biggest downsides to a fully client-side rendered app is performance, the user has to wait for ALL the JS to be downloaded, parsed, and executed before they can even see anything on the page.

Is this how the process works? If yes, it means one unnecessary call to the server.

Not quite. When a user browses to www.my-site.com for the first time their browser makes a request for the index.html page: GET /. The index.html is returned to the user's browser and the browser starts parsing the HTML. When it encounters <script> or <link> tags referring to JS or CSS assets, the browser will request those from the server: GET /bundle.js, GET /styles.css. That's it, no unnecessary requests. The Express routes are all middlewares, so they don't generate additional requests when they execute on a single incoming HTTP request.

I blogged about this awhile ago: http://ryanogles.by/conceptualizing-how-a-modern-single-page-app-is-served/

@cloudcompute
Copy link

I blogged about this awhile ago: http://ryanogles.by/conceptualizing-how-a-modern-single-page-app-is-served/

The sequence diagram has helped me understand "clearly" how SPAs actually work and what exactly happens after the user requesting the browser to load a SPA.

You helped me quite a lot.

@cloudcompute
Copy link

cloudcompute commented Jan 9, 2022

Hi @ryanoglesby08

One more query, Hope you'd answer.

Rewrites allow you to map an incoming request path to a different destination path. Next has built-in functionality for this. But how to implement it in React SPA.. just give an idea.

Source /api/*
Destination /index.html
Effect All requests → /index.html

Thanks

@ryanoglesby08
Copy link
Author

Hi @Ramandhingra,

Any server-side routing is done using standard Express features. Please check the docs for Express.
All client-side routing/rewriting/redirecting is done using React Router (or whatever client-side router you are using). Please check the docs for React Router.

In the future, please direct your questions to any of the community resources available, such as StackOverflow. https://www.reactiflux.com/ is a great learning tool for React. You could join the discord and ask questions there. Good luck!

@cloudcompute
Copy link

cloudcompute commented Jan 14, 2022

Hi @ryanoglesby08

Thank you so much for your reply and letting me know about these resources. In the future, if I have any questions, I will surely write over the discussion forums.

@shubhamkr1
Copy link

shubhamkr1 commented Feb 10, 2022

Hi @ryanoglesby08
what is the folder structure that is assumed that your project has, in order to use this piece of code ?

@TulshiDas39
Copy link

Thanks.

@aderchox
Copy link

Thanks.
(Just realized you haven't posted on your blog in the past 4 years, considering the quality of your content, I recommend continuing it, I've subscribed through RSS.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment