DEV Community

Braden Kelley for Okta

Posted on • Originally published at developer.okta.com on

Build a Basic CRUD App with Node and React

There are a lot of JavaScript frameworks out there today. It seems like I hear about a new one every month or so. They all have their advantages and are usually there to solve some sort of problem with an existing framework. My favorite to work with so far has been React. One of the best things about it is how many open source components and libraries there are in the React ecosystem, so you have a lot to choose from. This can be really difficult if you’re indecisive, but if you like the freedom to do things your way then React may be the best option for you.

In this tutorial, I’ll walk you through creating both a frontend web app in React and a backend REST API server in Node. The frontend will have a home page and a posts manager, with the posts manager hidden behind secure user authentication. As an added security measure, the backend will also not let you create or edit posts unless you’re properly authenticated.

The tutorial will use Okta’s OpenID Connect (OIDC) to handle authentication. On the frontend, the Okta React SDK will be used to request a token and provide it in requests to the server. On the backend, the Okta JWT Verifier will ensure that the user is properly authenticated, and throw an error otherwise.

The backend will be written with Express as a server, with Sequelize for modeling and storing data, and Epilogue for quickly creating a REST API without a lot of boilerplate.

Why React?

React has been one of the most popular JavaScript libraries for the past few years. One of the biggest concepts behind it, and what makes it so fast, is to use a virtual DOM (the Document Object Model, or DOM, is what describes the layout of a web page) and make small updates in batches to the real DOM. React isn’t the first library to do this, and there are quite a few now, but it certainly made the idea popular. The idea is that the DOM is slow, but JavaScript is fast, so you just say what you want the final output to look like and React will make those changes to the DOM behind the scenes. If no changes need to be made, then it doesn’t affect the DOM. If only a small text field changes, it will just patch that one element.

React is also most commonly associated with JSX, even though it’s possible to use React without JSX. JSX lets you mix HTML in with your JavaScript. Rather than using templates to define the HTML and binding those values to a view model, you can just write everything in JavaScript. Values can be plain JavaScript objects, instead of strings that need to be interpreted. You can also write reusable React components that then end up looking like any other HTML element in your code.

Here’s an example of some JSX code, that should be fairly simple to follow:

const Form = () => (
  <form>
    <label>
      Name
      <input value="Arthur Dent" />
    </label>
    <label>
      Answer to life, the universe, and everything
      <input type="number" value={42} />
    </label>
  </form>
);

const App = () => (
  <main>
    <h1>Welcome, Hitchhiker!</h1>
    <Form />
  </main>
);

Enter fullscreen mode Exit fullscreen mode

…and here’s what the same code would look like if you wrote it in plain JavaScript, without using JSX:

const Form = () => React.createElement(
  "form",
  null,
  React.createElement(
    "label",
    null,
    "Name",
    React.createElement("input", { value: "Arthur Dent" })
  ),
  React.createElement(
    "label",
    null,
    "Answer to life, the universe, and everything",
    React.createElement("input", { type: "number", value: 42 })
  )
);

const App = () => React.createElement(
  "main",
  null,
  React.createElement(
    "h1",
    null,
    "Welcome, Hitchhiker!"
  ),
  React.createElement(Form, null)
);

Enter fullscreen mode Exit fullscreen mode

I find the JSX form much easier to read, but that’s just like, you know, my opinion, man.

Create Your React App

The quickest way to get started with React is to use Create React App, a tool that generates a progressive web app (PWA) with all the scripts and boilerplate tucked away neatly behind something called react-scripts, so you can just focus on writing code. It has all kinds of nice dev features as well, like updating the code whenever you make changes, and scripts to compile it down for production. You can use npm or yarn, but I’ll be using yarn in this tutorial.

To install create-react-app and yarn, simply run:

npm i -g create-react-app@1.5.2 yarn@1.7.0

Enter fullscreen mode Exit fullscreen mode

NOTE : I’ll be adding version numbers to help future-proof this post. In general though, you’d be fine leaving out the version numbers (e.g. npm i -g create-react-app).

Now bootstrap your application with the following commands:

create-react-app my-react-app
cd my-react-app
yarn start

Enter fullscreen mode Exit fullscreen mode

The default app should now be running on port 3000. Check it out at http://localhost:3000.

Create React App default homepage

Create a Basic Homepage in React with Material UI

To keep things looking nice without writing a lot of extra CSS, you can use a UI framework. Material UI is a great framework for React that implements Google’s Material Design principles.

Add the dependency with:

yarn add @material-ui/core@1.3.1

Enter fullscreen mode Exit fullscreen mode

Material recommends the Roboto font. You can add it to your project by editing public/index.html and adding the following line inside the head tag:

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">

Enter fullscreen mode Exit fullscreen mode

You can separate components into separate files to help keep things organized. First, create a couple new folders in your src directory: components, and pages

mkdir src/components
mkdir src/pages

Enter fullscreen mode Exit fullscreen mode

Now create an AppHeader component. This will serve as the navbar with links to pages, as well as show the title and whether you’re logged in.

src/components/AppHeader.js

import React from 'react';
import {
  AppBar,
  Toolbar,
  Typography,
} from '@material-ui/core';

const AppHeader = () => (
  <AppBar position="static">
    <Toolbar>
      <Typography variant="title" color="inherit">
        My React App
      </Typography>
    </Toolbar>
  </AppBar>
);

export default AppHeader;

Enter fullscreen mode Exit fullscreen mode

Also create a homepage:

src/pages/Home.js

import React from 'react';
import {
  Typography,
} from '@material-ui/core';

export default () => (
  <Typography variant="display1">Welcome Home!</Typography>
);

Enter fullscreen mode Exit fullscreen mode

Now go ahead and actually just gut the sample app, replacing src/App.js with the following:

src/App.js

import React, { Fragment } from 'react';
import {
  CssBaseline,
  withStyles,
} from '@material-ui/core';

import AppHeader from './components/AppHeader';
import Home from './pages/Home';

const styles = theme => ({
  main: {
    padding: 3 * theme.spacing.unit,
    [theme.breakpoints.down('xs')]: {
      padding: 2 * theme.spacing.unit,
    },
  },
});

const App = ({ classes }) => (
  <Fragment>
    <CssBaseline />
    <AppHeader />
    <main className={classes.main}>
      <Home />
    </main>
  </Fragment>
);

export default withStyles(styles)(App);

Enter fullscreen mode Exit fullscreen mode

Material UI uses JSS (one of many flavors in the growingly popular trend of CSS in JavaScript), which is what withStyles provides.

The CssBaseline component will add some nice CSS defaults to the page (e.g. removing margins from the body), so we no longer need src/index.css. You can get rid of a couple other files too, now that we’ve gotten rid of most of the Hello World demo app.

rm src/index.css src/App.css src/logo.svg

Enter fullscreen mode Exit fullscreen mode

In src/index.js, remove the reference to index.css (the line that says import './index.css';). While you’re at it, add the following as the very last line of src/index.js to turn on hot module reloading, which will make it so that changes you make automatically update in the app without needing to refresh the whole page:

if (module.hot) module.hot.accept();

Enter fullscreen mode Exit fullscreen mode

At this point, your app should look like this:

Blank homepage

Add Authentication to Your Node + React App with Okta

You would never ship your new app out to the Internet without secure identity management, right? Well, Okta makes that a lot easier and more scalable than what you’re probably used to. Okta is a cloud service that allows developers to create, edit, and securely store user accounts and user account data, and connect them with one or multiple applications. Our API enables you to:

If you don’t already have one, sign up for a forever-free developer account. Log in to your developer console, navigate to Applications , then click Add Application. Select Single-Page App , then click Next.

Since Create React App runs on port 3000 by default, you should add that as a Base URI and Login Redirect URI. Your settings should look like the following:

Create new application settings

Click Done to save your app, then copy your Client ID and paste it as a variable into a file called .env.local in the root of your project. This will allow you to access the file in your code without needing to store credentials in source control. You’ll also need to add your organization URL (without the -admin suffix). Environment variables (other than NODE_ENV) need to start with REACT_APP_ in order for Create React App to read them, so the file should end up looking like this:

.env.local

REACT_APP_OKTA_CLIENT_ID={yourClientId}
REACT_APP_OKTA_ORG_URL=https://{yourOktaDomain}

Enter fullscreen mode Exit fullscreen mode

The easiest way to add Authentication with Okta to a React app is to use Okta’s React SDK. You’ll also need to add routes, which can be done using React Router. I’ll also have you start adding icons to the app (for now as an avatar icon to show you’re logged in). Material UI provides Material Icons, but in another package, so you’ll need to add that too. Run the following command to add these new dependencies:

yarn add @okta/okta-react@1.0.2 react-router-dom@4.3.1 @material-ui/icons@1.1.0

Enter fullscreen mode Exit fullscreen mode

For routes to work properly in React, you need to wrap your whole application in a Router. Similarly, to allow access to authentication anywhere in the app, you need to wrap the app in a Security component provided by Okta. Okta also needs access to the router, so the Security component should be nested inside the router. You should modify your src/index.js file to look like the following:

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Security } from '@okta/okta-react';

import App from './App';
import registerServiceWorker from './registerServiceWorker';

const oktaConfig = {
  issuer: `${process.env.REACT_APP_OKTA_ORG_URL}/oauth2/default`,
  redirect_uri: `${window.location.origin}/implicit/callback`,
  client_id: process.env.REACT_APP_OKTA_CLIENT_ID,
};

ReactDOM.render(
  <BrowserRouter>
    <Security {...oktaConfig}>
      <App />
    </Security>
  </BrowserRouter>,
  document.getElementById('root'),
);
registerServiceWorker();

if (module.hot) module.hot.accept();

Enter fullscreen mode Exit fullscreen mode

Now in src/App.js you can use Routes. These tell the app to only render a certain component if the current URL matches the given path. Replace your Home component with a route that only renders the component when pointing at the root URL (/), and renders Okta’s ImplicitCallback component for the /implicit/callback path.

src/App.js

--- a/src/App.js
+++ b/src/App.js
@@ -1,4 +1,6 @@
 import React, { Fragment } from 'react';
+import { Route } from 'react-router-dom';
+import { ImplicitCallback } from '@okta/okta-react';
 import {
   CssBaseline,
   withStyles,
@@ -21,7 +23,8 @@ const App = ({ classes }) => (
     <CssBaseline />
     <AppHeader />
     <main className={classes.main}>
- <Home />
+ <Route exact path="/" component={Home} />
+ <Route path="/implicit/callback" component={ImplicitCallback} />
     </main>
   </Fragment>
 );

Enter fullscreen mode Exit fullscreen mode

Next, you need a login button. This file is a bit bigger because it contains some logic to check if the user is authenticated. I’ll show you the whole component first, then walk through what each section is doing:

src/components/LoginButton.js

import React, { Component } from 'react';
import {
  Button,
  IconButton,
  Menu,
  MenuItem,
  ListItemText,
} from '@material-ui/core';
import { AccountCircle } from '@material-ui/icons';
import { withAuth } from '@okta/okta-react';

class LoginButton extends Component {
  state = {
    authenticated: null,
    user: null,
    menuAnchorEl: null,
  };

  componentDidUpdate() {
    this.checkAuthentication();
  }

  componentDidMount() {
    this.checkAuthentication();
  }

  async checkAuthentication() {
    const authenticated = await this.props.auth.isAuthenticated();
    if (authenticated !== this.state.authenticated) {
      const user = await this.props.auth.getUser();
      this.setState({ authenticated, user });
    }
  }

  login = () => this.props.auth.login();
  logout = () => {
    this.handleMenuClose();
    this.props.auth.logout();
  };

  handleMenuOpen = event => this.setState({ menuAnchorEl: event.currentTarget });
  handleMenuClose = () => this.setState({ menuAnchorEl: null });

  render() {
    const { authenticated, user, menuAnchorEl } = this.state;

    if (authenticated == null) return null;
    if (!authenticated) return <Button color="inherit" onClick={this.login}>Login</Button>;

    const menuPosition = {
      vertical: 'top',
      horizontal: 'right',
    };

    return (
      <div>
        <IconButton onClick={this.handleMenuOpen} color="inherit">
          <AccountCircle />
        </IconButton>
        <Menu
          anchorEl={menuAnchorEl}
          anchorOrigin={menuPosition}
          transformOrigin={menuPosition}
          open={!!menuAnchorEl}
          onClose={this.handleMenuClose}
        >
          <MenuItem onClick={this.logout}>
            <ListItemText
              primary="Logout"
              secondary={user && user.name}
            />
          </MenuItem>
        </Menu>
      </div>
    );
  }
}

export default withAuth(LoginButton);

Enter fullscreen mode Exit fullscreen mode

React components have a concept of state management. Each component can be passed props (in a component like <input type="number" value={3} />, type and number would be considered props). They can also maintain their own state, which has some initial values and can be changed with a function called setState. Any time the props or state changes, the component will rerender, and if changes need to be made to the DOM they will happen then. In a component, you can access these with this.props or this.state, respectively.

Here, you’re creating a new React component and setting the initial state values. Until you query the auth prop, you don’t know whether there’s a user or not, so you set authenticated and user to null. Material UI will use menuAnchorEl to know where to anchor the menu that lets you log the user out.

class LoginButton extends Component {
  state = {
    authenticated: null,
    user: null,
    menuAnchorEl: null,
  };

  // ...
}

Enter fullscreen mode Exit fullscreen mode

React components also have their own lifecycle methods, which are hooks you can use to trigger actions at certain stages of the component lifecycle. Here, when the component is first mounted you’ll check to see whether or not the user has been authenticated, and if so get some more details about the user, such as their name and email address. You also want to rerun this check whenever the component updates, but you need to be careful to only update the state when something is different, otherwise you’ll get yourself into an infinite loop (the component updates, so you give the component new values, which updates the component, you give it new values, etc.). The withAuth function is a Higher Order Component (HOC) which wraps the original component and returns another one containing the auth prop.

class LoginButton extends Component {
  // ...

  componentDidUpdate() {
    this.checkAuthentication();
  }

  componentDidMount() {
    this.checkAuthentication();
  }

  async checkAuthentication() {
    const authenticated = await this.props.auth.isAuthenticated();
    if (authenticated !== this.state.authenticated) {
      const user = await this.props.auth.getUser();
      this.setState({ authenticated, user });
    }
  }

  // ...
}

export default withAuth(LoginButton);

Enter fullscreen mode Exit fullscreen mode

The following functions are helper functions used later to log the user in or out, and open or close the menu. Writing the function as an arrow function ensures that this is referring to the instantiation of the component. Without this, if a function is called somewhere outside of the component (e.g. in an onClick event), you would lose access to the component and wouldn’t be able to execute functions on it or access props or state.

class LoginButton extends Component {
  // ...

  login = () => this.props.auth.login();
  logout = () => {
    this.handleMenuClose();
    this.props.auth.logout();
  };

  handleMenuOpen = event => this.setState({ menuAnchorEl: event.currentTarget });
}

Enter fullscreen mode Exit fullscreen mode

All React components must have a render() function. This is what tells React what to display on the screen, even if it shouldn’t display anything (in which case you can return null).

When you’re not sure of the authentication state yet, you can just return null so the button isn’t rendered at all. Once Okta this.props.auth.isAuthenticated() returns, the value will either be true or false. If it’s false, you’ll want to provide a Login button. If the user is logged in, you can instead display an avatar icon that has a dropdown menu with a Logout button.

class LoginButton extends Component {
  // ...

  render() {
    const { authenticated, user, menuAnchorEl } = this.state;

    if (authenticated == null) return null;
    if (!authenticated) return <Button color="inherit" onClick={this.login}>Login</Button>;

    const menuPosition = {
      vertical: 'top',
      horizontal: 'right',
    };

    return (
      <div>
        <IconButton onClick={this.handleMenuOpen} color="inherit">
          <AccountCircle />
        </IconButton>
        <Menu
          anchorEl={menuAnchorEl}
          anchorOrigin={menuPosition}
          transformOrigin={menuPosition}
          open={!!menuAnchorEl}
          onClose={this.handleMenuClose}
        >
          <MenuItem onClick={this.logout}>
            <ListItemText
              primary="Logout"
              secondary={user && user.name}
            />
          </MenuItem>
        </Menu>
      </div>
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

The next piece of the puzzle is to add this LoginButton component to your header. In order to display it on the right-hand side of the page, you can put an empty spacer div that has a flex value of 1. Since the other objects aren’t told to flex, the spacer will take up as much space as it can. Modify your src/components/AppHeader.js file like so:

src/components/AppHeader.js

--- a/src/components/AppHeader.js
+++ b/src/components/AppHeader.js
@@ -3,16 +3,27 @@ import {
   AppBar,
   Toolbar,
   Typography,
+ withStyles,
 } from '@material-ui/core';

-const AppHeader = () => (
+import LoginButton from './LoginButton';
+
+const styles = {
+ flex: {
+ flex: 1,
+ },
+};
+
+const AppHeader = ({ classes }) => (
   <AppBar position="static">
     <Toolbar>
       <Typography variant="title" color="inherit">
         My React App
       </Typography>
+ <div className={classes.flex} />
+ <LoginButton />
     </Toolbar>
   </AppBar>
 );

-export default AppHeader;
+export default withStyles(styles)(AppHeader);

Enter fullscreen mode Exit fullscreen mode

You should now be able to log in and out of your app using the button in the top right.

homepage with login button

When you click the Login button, you’ll be redirected to your Okta organization URL to handle authentication. You can log in with the same credentials you use in your developer console.

Okta sign in

Once successfully signed in, you’re returned back to your app and should now see an icon showing that you’re logged in. If you click on the icon, you’ll see your name in a logout button. Clicking the button keeps you on the homepage but logs you out again.

homepage, logged in

homepage without logout button

Add a Node REST API Server

Now that users can securely authenticate, you can build the REST API server to perform CRUD operations on a post model. You’ll need to add quite a few dependencies to your project at this point:

yarn add @okta/jwt-verifier@0.0.12 body-parser@1.18.3 cors@2.8.4 dotenv@6.0.0 epilogue@0.7.1 express @4.16.3 sequelize@4.38.0 sqlite@2.9.2
yarn add -D npm-run-all@4.1.3

Enter fullscreen mode Exit fullscreen mode

Create a new folder for the server under the src directory:

mkdir src/server

Enter fullscreen mode Exit fullscreen mode

Now create a new file src/server/index.js. To keep this simple we will just use a single file, but you could have a whole subtree of files in this folder. Keeping it in a separate folder lets you watch for changes just in this subdirectory and reload the server only when making changes to this file, instead of anytime any file in src changes. Again, I’ll post the whole file and then explain some key sections below.

src/server/index.js

require('dotenv').config({ path: '.env.local' });

const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const Sequelize = require('sequelize');
const epilogue = require('epilogue');
const OktaJwtVerifier = require('@okta/jwt-verifier');

const oktaJwtVerifier = new OktaJwtVerifier({
  clientId: process.env.REACT_APP_OKTA_CLIENT_ID,
  issuer: `${process.env.REACT_APP_OKTA_ORG_URL}/oauth2/default`,
});

const app = express();
app.use(cors());
app.use(bodyParser.json());

app.use(async (req, res, next) => {
  try {
    if (!req.headers.authorization) throw new Error('Authorization header is required');

    const accessToken = req.headers.authorization.trim().split(' ')[1];
    await oktaJwtVerifier.verifyAccessToken(accessToken);
    next();
  } catch (error) {
    next(error.message);
  }
});

const database = new Sequelize({
  dialect: 'sqlite',
  storage: './test.sqlite',
});

const Post = database.define('posts', {
  title: Sequelize.STRING,
  body: Sequelize.TEXT,
});

epilogue.initialize({ app, sequelize: database });

epilogue.resource({
  model: Post,
  endpoints: ['/posts', '/posts/:id'],
});

const port = process.env.SERVER_PORT || 3001;

database.sync().then(() => {
  app.listen(port, () => {
    console.log(`Listening on port ${port}`);
  });
});

Enter fullscreen mode Exit fullscreen mode

The following loads the environment variables we used in the React app. This way we can use the same env variables, and only have to set them in one place.

require('dotenv').config({ path: '.env.local' });

Enter fullscreen mode Exit fullscreen mode

This sets up the HTTP server and adds some settings to allow for Cross-Origin Resource Sharing (CORS) and will automatically parse JSON.

const app = express();
app.use(cors());
app.use(bodyParser.json());

Enter fullscreen mode Exit fullscreen mode

Here is where you check that a user is properly authenticated. First, throw an error if there is no Authorization header, which is how you will send the authorization token. The token will actually look like Bearer aLongBase64String. You want to pass the Base 64 string to the Okta JWT Verifier to check that the user is properly authenticated. The verifier will initially send a request to the issuer to get a list of valid signatures, and will then check locally that the token is valid. On subsequent requests, this can be done locally unless it finds a claim that it doesn’t have signatures for yet.

If everything looks good, the call to next() tells Express to go ahead and continue processing the request. If however, the claim is invalid, an error will be thrown. The error is then passed into next to tell Express that something went wrong. Express will then send an error back to the client instead of proceeding.

app.use(async (req, res, next) => {
  try {
    if (!req.headers.authorization) throw new Error('Authorization header is required');

    const accessToken = req.headers.authorization.trim().split(' ')[1];
    await oktaJwtVerifier.verifyAccessToken(accessToken);
    next();
  } catch (error) {
    next(error.message);
  }
});

Enter fullscreen mode Exit fullscreen mode

Here is where you set up Sequelize. This is a quick way of creating database models. You can Sequelize with a wide variety of databases, but here you can just use SQLite to get up and running quickly without any other dependencies.

const database = new Sequelize({
  dialect: 'sqlite',
  storage: './test.sqlite',
});

const Post = database.define('posts', {
  title: Sequelize.STRING,
  body: Sequelize.TEXT,
});

Enter fullscreen mode Exit fullscreen mode

Epilogue works well with Sequelize and Express. It binds the two together like glue, creating a set of CRUD endpoints with just a couple lines of code. First, you initialize Epilogue with the Express app and the Sequelize database model. Next, you tell it to create your endpoints for the Post model: one for a list of posts, which will have POST and GET methods; and one for individual posts, which will have GET, PUT, and DELETE methods.

epilogue.initialize({ app, sequelize: database });

epilogue.resource({
  model: Post,
  endpoints: ['/posts', '/posts/:id'],
});

Enter fullscreen mode Exit fullscreen mode

The last part of the server is where you tell Express to start listening for HTTP requests. You need to tell sequelize to initialize the database, and when it’s done it’s OK for Express to start listening on the port you decide. By default, since the React app is using 3000, we’ll just add one to make it port 3001.

const port = process.env.SERVER_PORT || 3001;

database.sync().then(() => {
  app.listen(port, () => {
    console.log(`Listening on port ${port}`);
  });
});

Enter fullscreen mode Exit fullscreen mode

Now you can make a couple small changes to package.json to make it easier to run both the frontend and backend at the same time. Replace the default start script and add a couple others, so your scripts section looks like this:

package.json

  "scripts": {
    "start": "npm-run-all --parallel watch:server start:web",
    "start:web": "react-scripts start",
    "start:server": "node src/server",
    "watch:server": "nodemon --watch src/server src/server",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }

Enter fullscreen mode Exit fullscreen mode

Now you can simply run yarn start and both the server and the React app will run at the same time, reloading whenever relevant changes are made. If you need to change the port for any reason, you can change the React app’s port and the server’s port with the PORT and SERVER_PORT environment variables, respectively. For example, PORT=8080 SERVER_PORT=8081 yarn start.

Add the Posts Manager Page to Your Node + React App

Now that you have a Node backend to manage your posts, you can link up the React frontend by adding another page. This will send requests to fetch, create, edit, and delete posts. It will also send the required authorization token along with each request so the server knows that you’re a valid user.

One nice thing about React Router is that it lets you use variables in the URL. This will allow us to use the ID of a post in the URL, so you could go to /posts/2 to view post number 2. With that in mind, you can create a modal that will be open whenever you are on that portion of the page, and to close the modal all you would need to do is navigate back to /posts.

Forms in React can be a bit of a pain. You can use a basic form element, but you would also need to listen for onChange events, update the state of the component, and set the new value on the input elements. To make forms easier, there are at least a few libraries out there, but I’ll show you how to use React Final Form to cut out a lot of the boilerplate.

You’ll also need recompose, lodash, and moment for some helper functions. You can install them all as dependencies with the following command:

yarn add react-final-form@3.6.4 final-form@4.8.3 recompose@0.27.1 lodash@4.17.10 moment@2.22.2

Enter fullscreen mode Exit fullscreen mode

Create a Post Editor Component

Create a PostEditor component which will be used in the Post Manager page. For now, the posts will just have title and body fields.

src/components/PostEditor.js

import React from 'react';
import {
  withStyles,
  Card,
  CardContent,
  CardActions,
  Modal,
  Button,
  TextField,
} from '@material-ui/core';
import { compose } from 'recompose';
import { withRouter } from 'react-router-dom';
import { Form, Field } from 'react-final-form';

const styles = theme => ({
  modal: {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  modalCard: {
    width: '90%',
    maxWidth: 500,
  },
  modalCardContent: {
    display: 'flex',
    flexDirection: 'column',
  },
  marginTop: {
    marginTop: 2 * theme.spacing.unit,
  },
});

const PostEditor = ({ classes, post, onSave, history }) => (
  <Form initialValues={post} onSubmit={onSave}>
    {({ handleSubmit }) => (
      <Modal
        className={classes.modal}
        onClose={() => history.goBack()}
        open
      >
        <Card className={classes.modalCard}>
          <form onSubmit={handleSubmit}>
            <CardContent className={classes.modalCardContent}>
              <Field name="title">
                {({ input }) => <TextField label="Title" autoFocus {...input} />}
              </Field>
              <Field name="body">
                {({ input }) => (
                  <TextField
                    className={classes.marginTop}
                    label="Body"
                    multiline
                    rows={4}
                    {...input}
                  />
                )}
              </Field>
            </CardContent>
            <CardActions>
              <Button size="small" color="primary" type="submit">Save</Button>
              <Button size="small" onClick={() => history.goBack()}>Cancel</Button>
            </CardActions>
          </form>
        </Card>
      </Modal>
    )}
  </Form>
);

export default compose(
  withRouter,
  withStyles(styles),
)(PostEditor);

Enter fullscreen mode Exit fullscreen mode

Create the Posts Manager Page Component

You’ll also need a page to render a list of posts, and to inject the post editor. Create a new file src/pages/PostsManager.js. Once again, I’ll post the whole file then walk you through each section.

src/pages/PostsManager.js

import React, { Component, Fragment } from 'react';
import { withAuth } from '@okta/okta-react';
import { withRouter, Route, Redirect, Link } from 'react-router-dom';
import {
  withStyles,
  Typography,
  Button,
  IconButton,
  Paper,
  List,
  ListItem,
  ListItemText,
  ListItemSecondaryAction,
} from '@material-ui/core';
import { Delete as DeleteIcon, Add as AddIcon } from '@material-ui/icons';
import moment from 'moment';
import { find, orderBy } from 'lodash';
import { compose } from 'recompose';

import PostEditor from '../components/PostEditor';

const styles = theme => ({
  posts: {
    marginTop: 2 * theme.spacing.unit,
  },
  fab: {
    position: 'absolute',
    bottom: 3 * theme.spacing.unit,
    right: 3 * theme.spacing.unit,
    [theme.breakpoints.down('xs')]: {
      bottom: 2 * theme.spacing.unit,
      right: 2 * theme.spacing.unit,
    },
  },
});

const API = process.env.REACT_APP_API || 'http://localhost:3001';

class PostsManager extends Component {
  state = {
    loading: true,
    posts: [],
  };

  componentDidMount() {
    this.getPosts();
  }

  async fetch(method, endpoint, body) {
    try {
      const response = await fetch(`${API}${endpoint}`, {
        method,
        body: body && JSON.stringify(body),
        headers: {
          'content-type': 'application/json',
          accept: 'application/json',
          authorization: `Bearer ${await this.props.auth.getAccessToken()}`,
        },
      });
      return await response.json();
    } catch (error) {
      console.error(error);
    }
  }

  async getPosts() {
    this.setState({ loading: false, posts: await this.fetch('get', '/posts') });
  }

  savePost = async (post) => {
    if (post.id) {
      await this.fetch('put', `/posts/${post.id}`, post);
    } else {
      await this.fetch('post', '/posts', post);
    }

    this.props.history.goBack();
    this.getPosts();
  }

  async deletePost(post) {
    if (window.confirm(`Are you sure you want to delete "${post.title}"`)) {
      await this.fetch('delete', `/posts/${post.id}`);
      this.getPosts();
    }
  }

  renderPostEditor = ({ match: { params: { id } } }) => {
    if (this.state.loading) return null;
    const post = find(this.state.posts, { id: Number(id) });

    if (!post && id !== 'new') return <Redirect to="/posts" />;

    return <PostEditor post={post} onSave={this.savePost} />;
  };

  render() {
    const { classes } = this.props;

    return (
      <Fragment>
        <Typography variant="display1">Posts Manager</Typography>
        {this.state.posts.length > 0 ? (
          <Paper elevation={1} className={classes.posts}>
            <List>
              {orderBy(this.state.posts, ['updatedAt', 'title'], ['desc', 'asc']).map(post => (
                <ListItem key={post.id} button component={Link} to={`/posts/${post.id}`}>
                  <ListItemText
                    primary={post.title}
                    secondary={post.updatedAt && `Updated ${moment(post.updatedAt).fromNow()}`}
                  />
                  <ListItemSecondaryAction>
                    <IconButton onClick={() => this.deletePost(post)} color="inherit">
                      <DeleteIcon />
                    </IconButton>
                  </ListItemSecondaryAction>
                </ListItem>
              ))}
            </List>
          </Paper>
        ) : (
          !this.state.loading && <Typography variant="subheading">No posts to display</Typography>
        )}
        <Button
          variant="fab"
          color="secondary"
          aria-label="add"
          className={classes.fab}
          component={Link}
          to="/posts/new"
        >
          <AddIcon />
        </Button>
        <Route exact path="/posts/:id" render={this.renderPostEditor} />
      </Fragment>
    );
  }
}

export default compose(
  withAuth,
  withRouter,
  withStyles(styles),
)(PostsManager);

Enter fullscreen mode Exit fullscreen mode

The backend is set to run on port 3001 on your local machine by default, so this sets that as a fallback. However, if you’d like to run this on another server, or on another port, you’ll need a way to edit that. You could run the app with API=https://api.example.com yarn start:web to override this.

const API = process.env.REACT_APP_API || 'http://localhost:3001';

Enter fullscreen mode Exit fullscreen mode

When the component first mounts, you won’t have any data yet. You may want some indicator that the page is still loading, so setting the state to loading: true lets you know that later on. Setting the initial posts to an empty array makes the code simpler later since you can just always assume you have an array, even if it’s empty. Then you’ll want to fetch the set of posts as soon as the component mounts.

class PostsManager extends Component {
  state = {
    loading: true,
    posts: [],
  };

  componentDidMount() {
    this.getPosts();
  }

  // ...
}

Enter fullscreen mode Exit fullscreen mode

Here you’re setting up a simple helper function to send a request to the server. This uses the fetch function that’s built into all modern browsers. The helper accepts a method (e.g. get, post, delete), an endpoint (here it would either be /posts or a specific post like /posts/3), and a body (some optional JSON value, in this case the post content).

This also sets some headers to tell the backend that any body it sends will be in JSON format, and it sets the authorization header by fetching the access token from Okta.

class PostsManager extends Component {
  // ...

  async fetch(method, endpoint, body) {
    try {
      const response = await fetch(`${API}${endpoint}`, {
        method,
        body: body && JSON.stringify(body),
        headers: {
          'content-type': 'application/json',
          accept: 'application/json',
          authorization: `Bearer ${await this.props.auth.getAccessToken()}`,
        },
      });
      return await response.json();
    } catch (error) {
      console.error(error);
    }
  }

  // ...
}

Enter fullscreen mode Exit fullscreen mode

Here is where you call the fetch helper function.

You have one function to fetch posts (getPosts), which will also set loading to false since it’s the function that gets called when the component first loads.

There’s another function to save posts, which handles the case of adding a new post as well as modifying an existing post. Since the posts will be loaded in a modal based on the route, once the post is updated the browser is told to go back to /posts.

The last function is to delete a post. The confirm function actually blocks the UI, so it’s not normally recommended for an app like this, but it works well for demo purposes. It’s a built-in browser function that simply gives a popup asking you to confirm, and returns either true or false depending on your answer.

After saving or deleting a post, the getPosts command is called again to make sure that all the posts are up to date.

class PostsManager extends Component {
  // ...

  async getPosts() {
    this.setState({ loading: false, posts: await this.fetch('get', '/posts') });
  }

  savePost = async (post) => {
    if (post.id) {
      await this.fetch('put', `/posts/${post.id}`, post);
    } else {
      await this.fetch('post', '/posts', post);
    }

    this.props.history.goBack();
    this.getPosts();
  }

  async deletePost(post) {
    if (window.confirm(`Are you sure you want to delete "${post.title}"`)) {
      await this.fetch('delete', `/posts/${post.id}`);
      this.getPosts();
    }
  }

  // ...
}

Enter fullscreen mode Exit fullscreen mode

The renderPostEditor function will be passed into a Route so that it only renders when you’re looking at a specific post. If you’re still loading posts, you won’t want to render anything just yet, so you can just return null. After the posts are loaded, you can use the id param to look for a post that matches. If you don’t find one for some reason, you should redirect back to the /posts page, since it’s likely a bad URL (maybe that post was already deleted).

The only exception is for a special route /posts/new, which will be used to create a new post. In that case, you don’t want to redirect. Now that you have a post model, you can render the PostEditor component from above and pass the model to it to render in a modal.

class PostsManager extends Component {
  // ...

  renderPostEditor = ({ match: { params: { id } } }) => {
    if (this.state.loading) return null;
    const post = find(this.state.posts, { id: Number(id) });

    if (!post && id !== 'new') return <Redirect to="/posts" />;

    return <PostEditor post={post} onSave={this.savePost} />;
  };

  // ...
}

Enter fullscreen mode Exit fullscreen mode

Here is the main render function. When there are no posts, it should display a message “No posts to display”, except when the posts are still loading. You could choose to render a loading symbol, but for now just rendering nothing will suffice.

When there are posts, it renders a simple list of them, with the main text being the title of the post, and some subtext saying when it was last updated. The updated text uses moment for rendering a user-friendly string like 10 minutes ago instead of the raw timestamp.

By adding component={Link} and the to value, you are actually turning the list item into a link that takes you to the path of the post (e.g. /posts/5). You can do the same to send you to create a new post, by creating the Floating Action Button (FAB) that you see on many Material Design apps.

class PostsManager extends Component {
  // ...

  render() {
    const { classes } = this.props;

    return (
      <Fragment>
        <Typography variant="display1">Posts Manager</Typography>
        {this.state.posts.length > 0 ? (
          <Paper elevation={1} className={classes.posts}>
            <List>
              {orderBy(this.state.posts, ['updatedAt', 'title'], ['desc', 'asc']).map(post => (
                <ListItem key={post.id} button component={Link} to={`/posts/${post.id}`}>
                  <ListItemText
                    primary={post.title}
                    secondary={post.updatedAt && `Updated ${moment(post.updatedAt).fromNow()}`}
                  />
                  <ListItemSecondaryAction>
                    <IconButton onClick={() => this.deletePost(post)} color="inherit">
                      <DeleteIcon />
                    </IconButton>
                  </ListItemSecondaryAction>
                </ListItem>
              ))}
            </List>
          </Paper>
        ) : (
          !this.state.loading && <Typography variant="subheading">No posts to display</Typography>
        )}
        <Button
          variant="fab"
          color="secondary"
          aria-label="add"
          className={classes.fab}
          component={Link}
          to="/posts/new"
        >
          <AddIcon />
        </Button>
        <Route exact path="/posts/:id" render={this.renderPostEditor} />
      </Fragment>
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

In order to get access to the Okta SDK, you need to use the withAuth HOC again. This time there are actually a few other HOCs to add, so you can use a utility function called compose from to wrap your component with multiple HOCs.

export default compose(
  withAuth,
  withRouter,
  withStyles(styles),
)(PostsManager);

Enter fullscreen mode Exit fullscreen mode

Add the Route and Navigation Links

OK, you’re in the home stretch now. You just need to tell the app when to render the Posts Manager page, and a link to get there.

Add the PostsManager page to src/App.js. Okta provides a SecureRoute component which is an extension of React Router’s Route component. This will ensure that if you try to go to that page and aren’t logged in, you’ll be redirected to sign in. If you’re on that page and you sign out, you’ll be redirected home.

src/App.js

--- a/src/App.js
+++ b/src/App.js
@@ -1,6 +1,6 @@
 import React, { Fragment } from 'react';
 import { Route } from 'react-router-dom';
-import { ImplicitCallback } from '@okta/okta-react';
+import { SecureRoute, ImplicitCallback } from '@okta/okta-react';
 import {
   CssBaseline,
   withStyles,
@@ -8,6 +8,7 @@ import {

 import AppHeader from './components/AppHeader';
 import Home from './pages/Home';
+import PostsManager from './pages/PostsManager';

 const styles = theme => ({
   main: {
@@ -24,6 +25,7 @@ const App = ({ classes }) => (
     <AppHeader />
     <main className={classes.main}>
       <Route exact path="/" component={Home} />
+ <SecureRoute exact path="/posts" component={PostsManager} />
       <Route path="/implicit/callback" component={ImplicitCallback} />
     </main>
   </Fragment>

Enter fullscreen mode Exit fullscreen mode

You also need to add a couple links to get to the Posts Manager and back to the Home page. You can do this in the App Header component you made earlier.

src/components/AppHeader.js

--- a/src/components/AppHeader.js
+++ b/src/components/AppHeader.js
@@ -1,6 +1,8 @@
 import React from 'react';
+import { Link } from 'react-router-dom';
 import {
   AppBar,
+ Button,
   Toolbar,
   Typography,
   withStyles,
@@ -20,6 +22,8 @@ const AppHeader = ({ classes }) => (
       <Typography variant="title" color="inherit">
         My React App
       </Typography>
+ <Button color="inherit" component={Link} to="/">Home</Button>
+ <Button color="inherit" component={Link} to="/posts">Posts Manager</Button>
       <div className={classes.flex} />
       <LoginButton />
     </Toolbar>

Enter fullscreen mode Exit fullscreen mode

Test your React + Node CRUD App

You now have a fully functioning Single Page App, connected to a REST API server, secured with authentication via Okta’s OIDC.

Go ahead and test out the app now. If they’re not already running, make sure to start the server and the frontend. In your terminal run yarn start from your project directory.

Navigate to http://localhost:3000. You should be able to add, edit, view, and delete posts to your heart’s desire!

new post

list of posts

Learn More About React, Node, and Okta

Hopefully you found this article helpful. If you’re new to React, maybe you’re one step closer to deciding whether you love it or hate it. If you’re a React veteran, maybe you found out how easy it can be to add authentication to a new or existing app. Or maybe you learned a bit about Node.

If you’d like to view the source code for the example application in this post, you can find it at https://github.com/oktadeveloper/okta-react-node-example.

If you’re still aching for more content, there is a plethora of great posts on the Okta developer blog. This post was not-so-loosely based on Build a Basic CRUD App with Vue.js and Node, which I would definitely recommend checking out if you’re interested in learning more about Vue.js. Here are some other great articles to check out as well:

And as always, we’d love to hear from you. Hit us up with questions or feedback in the comments, or on Twitter @oktadev.

Top comments (0)