-
Fork this repo on GitHub:
-
Rename your fork:
-
Clone your fork to your computer:
-
Copy
.env.sample
to.env
and modify it:$ cp .env.sample .env $ code .env
-
Launch the client and server in development mode:
$ yarn start:dev
First, let's look at how things are organized.
.
├── client
└── server
At the top level, everything is divided into two folders. The client
folder contains the React client, while the server
folder contains the Express server.
In the root folder, you'll also notice .gitignore
and package.json
. The former ignores node_modules
folders project-wide, while the latter contains scripts for development and deployment.
The client
folder is organized as follows:
client
├── public
└── src
├── components
│ ├── App
│ ├── AuthDropdown
│ ├── LoginForm
│ ├── Navigation
│ └── PrivateRoute
├── contexts
├── lib
└── pages
├── Home
├── Login
├── NotFound
└── Secret
The public
folder contains the one-and-only HTML file used on the client: index.html
. The only modification I made from the default is to add a link to the Bootstrap CDN.
The src
folder contains all the JavaScript code, organized into components
, contexts
, lib
, and pages
.
Inside the components
folder you'll find most of the React components used on the client:
The App
component is the "root" of the component tree. All other components (including those in the pages
folder, see below) are children of App
. This component also manages user authentication state, and provides the AuthContext
context allowing child components to easily access authentication state.
Used in the navigation bar, AuthDropdown
displays the currently logged in user (or a link to login/register) and allows them to logout. It consumes the AuthContext
provided by App
to access authentication state.
The LoginForm
component is used on the Login
page. It has no state, and is used to collect the user's email/password for authentication.
Then we have the Navigation
component, which is used at the top of the page to provide--you guessed it--navigation. As discussed above, it delegates to the AuthDropdown
component to manage authentication.
Lastly, we have the PrivateRoute
component. Never seen by the user, this is a high-order component that protects private routes from being accessed by logged-out users. It's used in App
to designate any routes that should not be publicly accessible, and consumes AuthContext
to know whether or not it should redirect.
AuthContext
provides authentication state and functions to modify that state. This is used by the App
component to pass authentication data to its children without prop-drilling. Any child component may consume this data as follows:
import AuthContext from '../../contexts/AuthContext';
const MyStatelessChildWithAuth = props => (
<AuthContext.Consumer>
{({ authToken, user, onLogin, onLogout }) => (
<div>{user.email}</div>
)}
</AuthContext.Consumer>
);
class MyStatefulChildWithAuth extends React.Component {
static contextType = AuthContext;
componentDidMount() {
API.someCall(this.context.authToken)
.then(res => console.log(res))
.catch(err => console.log(err));
}
render() {
return (
<div>{this.context.user.email}</div>
);
}
}
The API
module wraps all axios calls to the express server, providing one convenient place to look for anything API-related.
As you build out your app's features and add more API routes to your server, you will add corresponding client-side code in client/src/lib/API.js
. For every API route on your server, there should be a function in this module that hits the route and returns the result.
This is a personal preference, but I like to organize my "page" components into their own folder. These are the top-level components rendered by React Router when the user goes to a specific URL. Doing this separates "components" (which are intended to be reused) from pages (which generally aren't.)
The server
folder is organized as follows:
server
├── controllers
│ └── api
├── lib
└── models
Since React Router is handling all of our client-side routing, the only server routes we need to worry about are for our API. In this folder, you'll find those routes.
Divided into controllers, one for each model, they are all loaded into Express via server/controllers/api/index.js
. If you add a controller, make sure to import and register its URL-stub there.
Inside each controller, you'll see the routes to get, create, update and delete one kind of model:
usersController
has routes to register, login, and get the currently authenticated user.secretsController
has one route to get an array of Secrets (a dummy model I created to demonstrate protected data.)
Each time you create a new model, remember to create a new controller for its API routes. And remember to import and use it in index.js
.
This folder holds modules that don't belong anywhere else in the MVC structure. Currently, that's only one file: passport.js
.
Passport is used for user authentication, and for protecting API routes against unauthorized access. In server/lib/passport.js
, you'll find the primary Passport configuration for the server.
Importantly, the logic that verifies each user's authentication token resides here. Depending on which database you end up storing your users in, you'll want to customize this logic to query the correct user data.
In the master
branch, these models are just simple JS mocks that return hard-coded data. They are there to act as placeholders for a real database. See the sequelize
or mongoose
branches for Sequelize and Mongoose implementations, respectively.