https://github.com/peter-haro/node-express-sequelize-nextjs-realworld-example-app
Node.js + Express.js + Sequelize + SQLite/PostgreSQL + Next.js fullstack static/SSG/ISG Example Realworld App. Includes Heroku deployment, DB migration, DB and API unit testing, DB seeding, and ESLint linting setups.
https://github.com/peter-haro/node-express-sequelize-nextjs-realworld-example-app
expressjs fullstack-development next-js node-js postgresql real-world-problem-solving sequalizejs sqlite
Last synced: 20 days ago
JSON representation
Node.js + Express.js + Sequelize + SQLite/PostgreSQL + Next.js fullstack static/SSG/ISG Example Realworld App. Includes Heroku deployment, DB migration, DB and API unit testing, DB seeding, and ESLint linting setups.
- Host: GitHub
- URL: https://github.com/peter-haro/node-express-sequelize-nextjs-realworld-example-app
- Owner: Peter-Haro
- Created: 2025-09-09T02:43:25.000Z (8 months ago)
- Default Branch: deploy
- Last Pushed: 2025-09-09T03:11:53.000Z (8 months ago)
- Last Synced: 2025-09-09T05:02:07.145Z (8 months ago)
- Topics: expressjs, fullstack-development, next-js, node-js, postgresql, real-world-problem-solving, sequalizejs, sqlite
- Language: CSS
- Homepage:
- Size: 942 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.adoc
Awesome Lists containing this project
README
= Node.js + Express.js + Sequelize + SQLite/PostgreSQL + Next.js fullstack static/SSR/SSG/ISG Example Realworld App
:idprefix:
:idseparator: -
:sectanchors:
:sectlinks:
:sectnumlevels: 6
:sectnums:
:toc: macro
:toclevels: 6
:toc-title:
Includes <>, <>, <>, and <> setups.
This is a branch of https://github.com/Peter-Haro/node-express-sequelize-realworld-example-app rebased on top of the `master` branch of that repository, we show it in a separate GitHub repository just to give it more visibility.
This Full Stack app is part of the [RealWorld](https://github.com/gothinkster/realworld) project.
You might also check my [Laravel version](https://github.com/Peter-Haro/laravel-real-example) of this app.
You might also check my [Ruby on Rails version](https://github.com/Peter-Haro/ruby-on-rails-realworld-example-app) of this app.
Ideally we would like to have both demos on the same tree as they share all backend code, but Next.js and NPM impose too many restrictions which we don't have the time to work around.
The frontend part of this repository is a fork of https://github.com/reck1ess/next-realworld-example-app/tree/66003772a42bf71c7b2771119121d4c9cf4b07d4 which was SSR only via API, not SSG/ISG.
We decided to merge it in-tree with the API rather than keep it a separate submodule because the fullstack implementation means that the barrier between front-end and back-end becomes is quite blurred, e.g. "front-end" files under `/pages` also contain functions with direct backend database access without going through the API.
Both Next.js and the backend API run on the same Express.js server via a https://nextjs.org/docs/advanced-features/custom-server[Next.js custom server].
User-specific information such as "do I follow this article or not" is fetched by the client after it receives the user-agnostic statically generated page. The client then patches that page with this new user-specific information. See also: <>.
The design goals of this repository are similar to https://dev.to/givehug/next-js-apollo-client-and-server-on-a-single-express-app-55l6 except that we are trying to implement the Realworld App :-) GraphQL would be ideal, but its need is greatly diminished by SSR, which essentially forces manual ad-hoc implementation of GraphQL.
This repository is a baseline for the more realworld/advanced/specialized: https://github.com/cirosantilli/cirodown/tree/master/web We are trying to port back any problems that are noticed in that repository here. Some features will only be present there because of our constraint of not diverging from the Realworld spec.
toc::[]
== Local development with SQLite
....
npm install
npm run dev
....
You can now visit http://localhost:3000[] to view the website. Both API and pages are served from that single server.
You might also want to generate some test data as mentioned at: <>.
The SQLite database is located at `db.sqlite3`.
The Node.js and NPM versions this was tested with are specified at link:package.json[] `engines:` entry, which is what the <> uses https://devcenter.heroku.com/articles/nodejs-support#specifying-a-node-js-version[as mentioned here]. You should of course try to use those exact versions for reproducibility. The best way to install a custom node and NPM version is to use NVM as mentioned here: https://stackoverflow.com/questions/16898001/how-to-install-a-specific-version-of-node-on-ubuntu/47376491#47376491[].
== Local optimized frontend
....
npm install
npm run build-dev
npm run start-dev
....
If you make any changes to the code, at least code under `/pages` for sure, you have to rebuild before they take effect in this mode, as Next.js appears to also run server-only code such as `getStaticPaths` from one of the webpack bundles.
Running `next build` is one very important test of the code, as it builds many of the most important pages of the website, and runs checks such as TypeScript type checking. You should basically run it after every single commit that touches the frontend.
If you look into what `npm run start-dev` does in the `package.json`, you will see the following environment variables, which are custom to this project and not defined by Next.js itself:
* `NEXT_PUBLIC_NODE_ENV=development`: sets everything to be development by default.
+
If this variable not given, `NODE_ENV` is used instead.
+
Just like `NODE_ENV`, this variable affects the following aspects of the application:
+
** if the Next.js server will run in development or production mode. From the Next.js CLI, this determination is done with `next dev` vs `next start`. But we use a custom server where both dev and prod are run from `./app`, and so we determine that from environment variables.
** if the database will be SQLite (default development DB) or PostgreSQL (default production DB)
** in browser effects, e.g. turns off Google Analytics
+
We cannot use `NODE_ENV` here directly as we would like because and Next.js forces `process.env.NODE_ENV` to match the server's dev vs production mode. But we want a production mode server, and no Google analytics in this case.
* `NODE_ENV_NEXT_SERVER_ONLY=production`: determines is the Next.js server will run in development or production mode.
+
This variable only affects the Next.js server dev vs prod aspect of the application, and not any other aspects such as the database used and in browser effects such as having Google Analytics or not.
+
If given, this variable overrides all others in making that determination, including `NEXT_PUBLIC_NODE_ENV`. If not given, `NODE_ENV` is used as usual.
+
If this variable is not given, `NEXT_PUBLIC_NODE_ENV` is given instead.
=== Local run as identical to deployment as possible
Here we use PostgreSQL instead of SQLite with the prebuilt static frontend. Note that optimized frontend is also used on the SQLite setup described at <>).
For when you really need to debug some deployment stuff locally
Setup:
....
sudo apt install postgresql
# Become able to run psql command without sudo.
sudo -u postgres createuser -s "$(whoami)"
createdb "$(whoami)"
createdb realworld_next
psql -c "CREATE ROLE realworld_next_user with login password 'a'"
psql -c 'GRANT ALL PRIVILEGES ON DATABASE realworld_next TO realworld_next_user'
echo "SECRET=$(tr -dc A-Za-z0-9 > .env
....
Run:
....
npm run build-prod
npm run start-prod
....
then visit the running website at: http://localhost:3000/
To <> for this instance run:
....
npm run seed-prod
....
=== Development run in PostgreSQL
If you want to debug a PostgreSQL specific issue interactively on the browser, you can run a development Next.js server on PostgreSQL.
This is similar to <>, but running the development server is more convenient for development as you won't have to `npmr run build-prod` on every frontend change.
First setup the PostgreSQL database as in <>.
Then start the server with:
....
npm run dev-pg
....
To run other database related commands on PostgreSQL you can export the `REALWORLD_PG=true` environment variable manually as in:
....
REALWORLD_PG=true ./bin/sync-db.js
REALWORLD_PG=true ./bin/generate-demo-data.js
....
If you need to inspect the database manually you can use:
....
psql realworld_next
....
== Heroku deployment
The setup is analogous to: https://github.com/Peter-Haro/node-express-sequelize-realworld-example-app#heroku-deployment but instead of `heroku git:remote -a cirosantilli-realworld-express` you should use:
....
git remote add heroku-next https://github.com/Peter-Haro/node-express-sequelize-nextjs-realworld-example-app.git
./heroku.sh addons:create --app cirosantilli-realworld-next heroku-postgresql:hobby-dev
./heroku.sh config:set --app cirosantilli-realworld-next SECRET="$(tr -dc A-Za-z0-9 >. This is because before deployment we always run the tests to ensure that nothing broke. And running the tests in PostgreSQL is particularly crucial, because since Sequelize is not stellar, sometimes things work in SQLite but fail in PostgreSQL.
Then, to deploy the latest commit run:
....
npm run deploy
....
This also pushes the code to GitHub on success, and markes the deployed commit with the `deploy` branch on GitHub, to mark clearly the deployed commit clearly in case local development moves ahead of the deployed commit a bit.
You then have to add `--app cirosantilli-realworld-next` to any raw `heroku` commands to allow Heroku to differentiate between them, e.g.:
....
./heroku.sh run --app cirosantilli-realworld-next bash
....
for which we have the helper:
....
./heroku.sh run bash
....
e.g. to delete, recreate and reseed the database:
....
./heroku.sh run bin/generate-demo-data.js --force-production
....
We are not sure if Next.js ISR can be deployed reliably due to the ephemeral filesystem such as those in Heroku...: https://stackoverflow.com/questions/67684780/how-to-set-where-the-prerendered-html-of-new-pages-generated-at-runtime-is-store but it has worked so far.
=== `devDependencies` vs `dependencies`
Note that any dependencies required only for the build e.g. typescript are put under `devDependencies`.
Our current <> setup installs both `dependencies` and `devDependencies`, builds, and then removes `devDependencies` from the deployed code to make it smaller.
=== Demo mode
Activated with `NEXT_PUBLID_DEMO=true` or:
....
npm run dev-demo
....
This has the following effects:
* block posts with tags given at `blacklistTags` of `config.js` The initial motivation for this was to block automated "Cypress Automation" spam that is likely setup by some bastard on all published implementations via the backend, example: https://archive.ph/wip/n4Jlx[], and might be taking up a good part of our Heroku dynos, to be confirmed.
+
We've logged their IP as 31.183.168.37, let's see if it changes with time. That IP is from Poland, which is consistent with Google Analytics results, which are overwhelmingly from Poland, suggesting a bot within that country, which also does GET on the web UI.
* whenever a new object is created, such as article, comment or user, if we already have 1000 objects of that type, delete the oldest object of that type, so as to keep the database size limited. TODO implement for Tags, Follows and Likes.
* "Source code for this website" banner on top with link to this repository
* clearer tags input message "Press Enter, Tab or Comma to add a tag"
=== Heroku instance management
Get a PostgreSQL shell:
....
./heroku.sh psql
....
or run a one-off Postgres query:
....
./heroku.sh psql -c 'SELECT * FROM "User"'
....
DELETE ALL DATA IN THE DATABASE and <> inside Heroku:
....
./heroku.sh run bash
....
and then run in that shell:
....
bin/generate-demo-data.js --force-production
....
or you can do it in one go with:
....
./heroku.sh run bin/generate-demo-data.js --force-production
....
We have to run `heroku run bash` instead of `heroku ps:exec` because the second command does not set `DATABASE_URL`:
* https://stackoverflow.com/questions/62502951/heroku-env-variables-database-url-and-port-not-showing-in-dyno-heroku-psexec/68050303#68050303
* https://stackoverflow.com/questions/48119289/how-to-get-environment-variables-in-live-heroku-dyno/64951959#64951959
* https://www.reddit.com/r/rails/comments/ejljxj/how_to_seed_a_postgres_production_database_on/
Edit a file in Heroku to debug that you are trying to run manually, e.g. by adding print commands, uses https://github.com/hakash/termit[] minimal https://en.wikipedia.org/wiki/GNU_nano[nano]-like text editor:
....
./heroku.sh ps:exec
termit app.js
....
== ISR vs SSR
ISR is an optimization that aims to:
* reduce load times
* reduce server load
Like all optimizations, it makes things more complex, so you really have to benchmark things to see if you need them.
As mentioned at: <>, this is one of the main goals of this website.
The main complexity increase of ISR is that you have to worry about React `usEffect` chains of events after the initial page load, which can be very hard to debug.
With ISR, we want article contents and user pages to load instantly from a prerendered cache, as if the user were logged out.
Only after that will login-specific details be filled in by client JavaScript requests to the backend API, e.g. "have I starred/favorited this article or not".
This could lead to amazing article text loading performance, since this is the same for all users and can be efficiently cached.
The downside of that is that the user could see a two stage approach which is annoying, especially if there is no clear indication (first download, then wait, then if updates with personal details). This could be made even better by caching things client side, and `userSWR` which we already using likely makes that transparent, so there is hope. Even more amazing would be if it could cache across requests, e.g. from index page to an article! One day, one day, maybe with GraphQL.
Another big ISR limitation is that you can't force a one-off immediate page update after the user edits a post, a manual refresh is generally needed: https://github.com/vercel/next.js/discussions/25677[]. However, this is not noticeable in this website, because in order to show login-specific information, we are already re-fetching the data from the API after every single request, so after a moment it gets updated to the latest version.
Our organizational principle is that all logged-in API data will be fetched from the toplevel element of each page. It will have the exact same form as the static rendering props, which come directly from the database during build rather than indirectly the API.
This data will correspond exactly to the static prerendered data, but with the user logged in. It will then simply replace the static rendered logged out version, and trigger a re-render.
This approach feels simple enough that it could even be automated in a framework manner. One day, one day.
It is true that the pass-down approach goes a bit against the philosophy of `useSWR`, but there isn't much we can do, e.g. `/` fetches all articles with `/api/articles`, and determines favorite states of multiple posts. Therefore, we if hardcoded `useSWR` for the article under `FavoriteArticleButton`, that would fetch the states from each article separately `/api/articles/[slug]`. We want that to happen on single article inspection however.
=== SSR version
We are slowly building an SSR version of the website under the `/ssr` prefix. E.g. `/ssr` will be a SSR version of the ISR at `/`, `/ssr/login` of `/login`, and so on.
The most noticeable thing in SSR is if you open the DevTools that there are no `GET` requests to the `/api` after the page loads, except where we are forced to do them by the terrible design of Realworld not having separate URLs for pagination and some tabs.
You will also never see the loading spinner. The page will just load all at once in one go.
This will allows us to very directly compare if there are any noticeable user experience differences.
TODO It would also be amazing to test server overload with this, but that is harder. One day.
=== No flickering and automatic updates
Our general ISR philosophy is: the only flickering or automatic page update allowed is from loading spinner to the final data.
New data can only ever happen if the user presses F5.
We do have one exception though: the front page, as it would be too confusing for users to not see their newly created post there. An update might happen on that page therefore.
This is the kind of thing that suggests that SSR is generally what you want for index/find pages.
== What works with JavaScript turned off
Due to ISR/SSR, <>, which includes e.g. articles and profiles but not "Your Feed" vs "Global Feed, look exactly the same with and without JavaScript for a logged out user.
For the pages without distinct URLs, we don't know how to do this, the only way we can do it is by fetching the API with JavaScript.