All the directory structure, dev tools, boilerplate, and pipeline stuff that I find myself doing every time I make a new web app. This is a template repo from which you can build your own app.
In approximate order of significance:
- TypeScript
- All code except the
scripts/
directory and some configuration files are to be written in TypeScript instead of JavaScript.
- All code except the
- React
- Always use functional components and hooks.
- Version 18 here, but downgradable if necessary. You may run into issues with peer dependencies when trying to downgrade. You'll have to force npm to install the desired version, or remove a couple packages, downgrade, and reinstall them.
- Express
- Not much to say here. See project structure below for more details on how server code is organized.
- React Context (as primary global state management)
- The
useContext
anduseReducer
hooks are used to keep track of app-level state. You can define actions inclient/js/context/actions/
and dispatch them from any component to update the context. - Also included is a special reducer which can run middlewares, and a middleware for handling thunks, actions which are functions so that you can, for example, do an api call between calling
dispatch
and updating state. - Multiple stores using different chains of middlewares can be used in the same app.
- The
- Webpack
- Comes fully configured for typescript, babel, and less in separate development and production builds
- In development, typescript files include source maps and hot module replacement
- Babel
.babelrc
is picked up by webpack and comes with react preset, but fairly rudimentary as-is.
- Less
- For styling with imports/variables/etc...
- Strongly prefer all styles to go in less files in
client/css/
, and only set class names in code. - Easily swapped out for sass or scss other by modifying
webpack.common.js
.
- Jest
- All tests go in a parallel folder structure within
test/
. - Separately configured for client and server.
- Uses
ts-jest
, so code can be imported directly as TypeScript.
- All tests go in a parallel folder structure within
- ts-standard
- An extremely opinionated linter. Sometimes frustrating in how oppressive it is, but for good reason.
- Webpack Hot Module Replacement
- Also necessarily including
webpack-dev-middleware
- All client-side code is compiled by webpack into memory, and served from memory instead of disk, so there is no need to build before running.
- Only imported in development environment
- Will watch for changes in the
client/
directory and if any are detected, will recompile into code and send the updated code to the client automatically, meaning as soon as you save a file, the updates will be reflected in your browser instantly without restarting the server or refreshing.
- Also necessarily including
- Husky
- A tool for easily managing git hooks so they can be checked in per-repository and enforce, for example, running tests and linting in a pre-commit hook.
- Winston
- Includes a built-in logger already configured for http logging.
- Docker
- Just a Dockerfile doing the bare minimum, but a good reference and foundation.
Below is a layout of the directory tree with quick one-liners for the various folders and files included. Some files are scaffolding and some are examples. Modify and delete at your will, but it may be worth keeping even some of the example code around for reference until you've effectively replaced it.
/
├── .husky/ Scripts run as git hooks (see Husky docs)
│ └── pre-commit Runs tests and linter
├── client/
│ ├── css/ All styles go here, not in the components
│ │ └── components/ Try to separate styles by component
│ ├── js/
│ │ ├── components/ Reusable components
│ │ ├── context/ Context providers
│ │ │ ├── actions/ Actions used to indicate state changes in the app
│ │ │ │ └── Action.ts A class definition of an Action which actions should extend
│ │ │ ├── middlewares/ Middlewares to handle actions before the reducer is called
│ │ │ └── reducers/ Reducers to modify context based on actions
│ │ ├── hooks/ Any custom React hooks you want to use
│ │ ├── lib/ Client-side code not directly related to rendering
│ │ ├── routes/ Pages to be plugged into the router
│ │ ├── types/ Type definitions for the client
│ │ ├── App.tsx Maps routes to components with `react-router-dom`
│ │ └── index.tsx Attaches context providers
│ ├── public/ Static resources like images
│ │ └── index.html The html file the app will be rendered into
│ └── tsconfig.json Client-side TypeScript config
├── config/ Configuration files for malleable but permanent data
├── scripts/ Any scripts too complex for package.json
├── server/
│ ├── controllers/ Behaviors to be attached to routes
│ ├── lib/ Code or modules required by the server which are
│ │ too big or complex for controllers/ or utils/
│ ├── middleware/ Any custom middleware
│ │ ├── errorLogger.ts Logs uncaught errors during a request
│ │ └── httpLogger.ts Logs incoming requests and info about their responses
│ ├── routes/ Routes attach controllers to paths
│ ├── services/ Interfaces for interacting with anything not in this codebase
│ │ (REST api wrappers, database access, etc...)
│ ├── util/ Pure, reusable functions
│ │ └── logger.ts A Winston logger (see docs and/or look at examples in this repo)
│ └── index.ts Starts the server, attaches middleware and routes
├── test/ Tests structured in the same manner as the project
├── .babelrc
├── .dockerignore
├── .env Environment config (create this yourself from example.env)
│ for NODE_ENV, database url, credentials, etc...
├── .gitignore
├── Dockerfile Very barebones for creating docker containers
├── index.ts Loads the environment config and starts the server
│ This is where you'd spawn any other necessary
│ processes that are separate from the server
├── package-lock.json
├── package.json
├── README.md Hello
├── tsconfig.base.json Common Typescript configuration between the server and client
├── tsconfig.json Server-side Typescript config (does NOT affect client/)
├── webpack.common.js Common webpack configuration
├── webpack.dev.js Dev-specific webpack config
└── webpack.prod.js Production webpack config
Remember to create a .env
file with NODE_ENV
set to development
. Otherwise you'll probably have build issues, or at the very least you won't get things like hot module replacement and debug logging. The .env
file shouldn't be committed, it should be used for configuration specific to the system running the server. You should also go through package.json
and make sure all the fields are filled and correct.
This entire repository is nothing but a manifestation of my opinions, so feel free to disagree with, tweak, or embrace anything. You could even fork it and make your own.
When you're in a directory that exports an arbitrary number of similar things, you should not use default exports. This may sound questionable at first, but it enforces stricter conventions when it comes to importing. A directory should have an index.ts
file which re-exports all the other stuff in its directory. Then another file can import the whole group of related functions and objects, and destructure them out of the imported object. For example, this
import { Button, Checkbox, Card } from '../components'
import { base64Encode, base64Decode, isUnicorn } from '../util'
is more convenient and readable than this
import Button from '../components/Button'
import Checkbox from '../components/Checkbox'
import Card from '../components/Card'
import base64Encode from '../util/base64Encode'
import base64Decode from '../util/base64Decode'
import isUnicorn from '../util/isUnicorn'
Another benefit is that the names of exported objects are enforced. Someone can't accidentally import Buton from '../components/Button
anymore because import { Buton } from '../components'
will fail.
This section is for the scripts in package.json
.
Project management
postinstall
- Sets uphusky
onnpm install
clean
- Removes build filesclean:all
- Removes build files, node modules, and Husky scripts. Essentially reset to a freshgit clone
(with the notable exception of.env
)
Running the app
dev
- Starts the server (development). All server code will be run directly from source usingts-node
, and all client-side code will be compiled on the fly and served from memory, so it is not necessary to build anything before running this command. It's expected thatNODE_ENV
is set todevelopment
or this will not work as expected.start
- Starts the server (production). You must runnpm run build
before running this command, as it will only use files indist/
. It's generally expected that if you're running this command, you're in a production setting andNODE_ENV
is set toproduction
, but it will still work and still have some development features enabled ifNODE_ENV
is set todevelopment
.
Tests
test
- Runs both client and server teststest:client
test:server
Linting
lint
- Find linting errors in server and client codelint:fix
- Find linting errors and also try to fix themlint:client
- Find linting errors in client code (trynpm run lint:client -- --fix
to fix only client errors)lint:server
- Find linting errors in server code (trynpm run lint:server -- --fix
to fix only server errors)
Building
build
- Builds client and server code into thedist/
directorybuild:client
- Runs webpack, which will compile typescript, transpile to es5, and bundle. Also grabs other resources like images or stylesheets. Outputs todist/client/
build:server
- Runs the typescript compiler on server code and outputs todist/server
(plus the rootindex.ts
)
I often run or test web apps at subroutes of one domain, so this basepath
config is a singular location to keep that subroute in line for the whole app. For example:
https://domain.tld/ - App A
https://domain.tld/foo - App A
https://domain.tld/bar - App B
https://domain.tld/baz - App C
https://domain.tld/baz/test - App C
Here, App A
would have a basepath
of ""
, App B
would have a basepath
of "/bar"
, and App C
would have a basepath
of "/baz"
. I can have a reverse router point incoming traffic to the correct app, and each app is individually able to keep webpack
, react-router
, and express
in line with each other based on the value of basepath
in the config.
If you don't intend your app to run under some subroute, it's probably easier to keep it set to an empty string than to try to remove basepath
and its uses from the code.
NOTE: Setting basepath
to "/"
is not the same as setting it to ""
and will cause issues.
The code in this project is not a web app, it is the source code to a web app. That code needs to be compiled into a distributable which is then run in production.
Client-side code is compiled into bundles akin to a binary executable which axios
is already included in. So to list axios
as a dependency of the web app is unnecessary. Once the build is complete, axios
no longer needs to be installed for the app to function. In that sense, axios
is like a static library that was packaged into our "executable".
Server-side code is also compiled into an "executable". It so happens that this executable is more or less readable by humans and still has to be run through an interpreter, but it isn't intended to be modified or inherently readable, it's just the stuff that runs in production which was produced by compiling source code. However, unlike on the client, packages like express
and winston
are not included in this build. These are like dynamically linked libraries, and our production app is dependent on them in order to function.
In short, dependencies
are packages that are absolutely required at runtime whereas devDependencies
are only required to write and test the source code. This creates a material difference between the two. Once the app is built, you can remove the packages required by devDependencies
and still have a perfectly functional app, something the Dockerfile does.
You can make an argument for instead delineating between them by whether something is a "development tool" or "code that runs in production", but there is no function to that separation aside from making that distinction for its own sake. This basically only letsyou answer the question "Do we use this package in the client-side code, or only in our development scripts?" You're welcome to make that change yourself anyway if you like. It will not affect anything except the size of the resulting Docker image.