Coder Social home page Coder Social logo

express-api-react's Introduction

SOFTWARE ENGINEERING IMMERSIVE

Getting Started

  1. Fork
  2. Clone

Express API - React CRUD

We've built the backend. Now it's time to build the frontend.

In this project we are going to be using react to build a CRUD frontend to our items json api.

cd express-api-react
npm install

Checkout the database config:

config/config.json

{
  "development": {
    "database": "items_app_development",
    "dialect": "postgres"
  },
  "test": {
    "database": "items_app_test",
    "dialect": "postgres"
  },
  "production": {
    "use_env_variable": "DATABASE_URL",
    "dialect": "postgres",
    "dialectOptions": {
      "ssl": true
    }
  }
}

Now run $ npm run db:reset to set up your database.

Make sure the data exists:

psql items_app_development
SELECT * FROM "Items";

Run the server:

npm start

Test the following routes in your browser:

Now open a new tab in the terminal. Make sure you're inside the repo.

Let's create our React app.

npx create-react-app client

Let's start by adding react router:

cd client
npm install react-router-dom

Important: Notice how there are two package.json's one in the root of the repo for the server, and the other inside the client folder. Make sure you're inside the client folder. We want to install the react router package so we can use it for the react app.

And now let's setup our app to use react router:

client/src/index.js

import { BrowserRouter as Router } from "react-router-dom";

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  document.getElementById("root")
);

Cool. Now let's setup our routes. A route will render an associated component. Below is the list:

/ - the homepage, just display a welcome screen. It will render a Home component.

/items - the ability to see all items. It will render an Items component.

/create-item - the ability to create a new item. It will render an ItemCreate component.

/items/:id - the ability to see a specific item. It will render an Item component.

/items/:id/edit - the ability to edit an item. It will render an ItemEdit component.

Let's start by creating our empty components in client/src directory:

mkdir components
cd components
mkdir routes
cd routes
touch Home.jsx Item.jsx ItemCreate.jsx ItemEdit.jsx Items.jsx

Now let's create our routes:

client/App.js

import React from 'react'
import { Route } from 'react-router-dom'

import Items from './components/routes/Items'
import Item from './components/routes/Item'
import ItemEdit from './components/routes/ItemEdit'
import ItemCreate from './components/routes/ItemCreate'
import Home from './components/routes/Home'

const App = () => (
  <React.Fragment>
    <Route exact path='/' component={Home} />
    <Route exact path='/items' component={Items} />
    <Route exact path='/create-item' component={ItemCreate} />
    <Route exact path='/items/:id' component={Item} />
    <Route exact path='/items/:id/edit' component={ItemEdit} />
  </React.Fragment>
)

export default App

A simple Home component: src/components/routes/Home.jsx

import React from 'react'
import Layout from '../shared/Layout'

const Home = () => (
  <Layout>
    <h4>Welcome to the items app!</h4>
  </Layout>
)

export default Home

Notice the Layout component. We are going to build the Layout component next. This is a shared component that we will re-use multiple times. Essentially, the Layout component is the shell of the web app we are building.

Let's create our "shared" components. The idea of shared components is that anytime we have code that we would repeat in several components (a footer, a navbar, etc), we can wrap that code inside a component and import it in whenever needed.

cd client/src/components
mkdir shared
cd shared
touch Layout.jsx Footer.jsx Nav.jsx

Let's start with the Layout component:

components/shared/Layout.jsx

import React from 'react'

import Nav from './Nav'
import Footer from './Footer'

const Layout = props => (
  <div>
    <h1>Items App</h1>
    <Nav />

    {props.children}

    <Footer />
  </div>
)

export default Layout

Note: We are using props.children here. React Children is a placeholder for which ever component calls the component that props.children is in. You will see this in action in a minute.

Let's create our Nav component:

components/shared/Nav.jsx

import React from 'react'
import { NavLink } from 'react-router-dom'

const Nav = () => (
  <nav>
    <NavLink to='/'>Home</NavLink>
    <NavLink to='/items'>Items</NavLink>
    <NavLink to='/create-item'>Create Item</NavLink>
  </nav>
)

export default Nav

And the Footer component:

components/shared/Footer.jsx

import React from 'react'

const Footer = () => (
  <p>© Copyright {new Date().getFullYear()}. All Rights Reserved.</p>
)

export default Footer

Let's make sure the app is working.

cd express-api-react
npm start

Open a new tab in your terminal and run your client:

cd client
npm start

Open your browser and test the route http://localhost:3001/. The Home component should render but the other links will not work yet because we haven't built them out yet.

Cool. We are done with shared components for now.

Now let's build the Items component.

We will be making an axios call in the Items component to fetch all the Items from the server.

Let's start by installing axios. Make sure you're in the client folder.

cd client
npm install axios

When you run npm install axios, make sure you're inside the client folder where the package.json exists.

Now we can build the Items component:

import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'

class Items extends Component {
  constructor (props) {
    super(props)

    this.state = {
      items: []
    }
  }

  async componentDidMount () {
    try {
      const response = await axios(`http://localhost:3001/api/items`)
      this.setState({ items: response.data.items })
    } catch (err) {
      console.error(err)
    }
  }

  render () {
    const items = this.state.items.map(item => (
      <li key={item.id}>
        <Link to={`/items/${item.id}`}>{item.title}</Link>
      </li>
    ))

    return (
      <>
        <h4>Items</h4>
        <ul>
          {items}
        </ul>
      </>
    )
  }
}

export default Items

Test the http://localhost:3001/api/items route in your browser.

Good? Great. Let's move on to the Item component.

components/routes/Item.jsx

import React, { Component } from 'react'
import { Link, Redirect } from 'react-router-dom'
import axios from 'axios'

import Layout from '../shared/Layout'

class Item extends Component {
  constructor(props) {
    super(props)

    this.state = {
      item: null,
      deleted: false
    }
  }

  async componentDidMount() {
    try {
      const response = await axios(`http://localhost:3001/api/items/${this.props.match.params.id}`)
      this.setState({ item: response.data.item })
    } catch (err) {
      console.error(err)
    }
  }

  destroy = () => {
    axios({
      url: `http://localhost:3001/api/items/${this.props.match.params.id}`,
      method: 'DELETE'
    })
      .then(() => this.setState({ deleted: true }))
      .catch(console.error)
  }

  render() {
    const { item, deleted } = this.state

    if (!item) {
      return <p>Loading...</p>
    }

    if (deleted) {
      return <Redirect to={
        { pathname: '/', state: { msg: 'Item succesfully deleted!' } }
      } />
    }

    return (
      <Layout>
        <h4>{item.title}</h4>
        <p>Link: {item.link}</p>
        <button onClick={this.destroy}>Delete Item</button>
        <Link to={`/items/${this.props.match.params.id}/edit`}>
          <button>Edit</button>
        </Link>
        <Link to="/items">Back to all items</Link>
      </Layout>
    )
  }
}

export default Item

We should now be able to see http://localhost:3001/items/1.

Next, we want to implement the ItemEdit and ItemCreate. Inside the ItemEdit component we will have a form to edit an item. And Inside the ItemCreate component we will have form to create an item. What if we could abstract those two forms into one? We can, so let's do that now by creating another shared component called ItemForm:

cd components/shared/
touch ItemForm.jsx

components/shared/ItemForm.jsx

import React from 'react'
import { Link } from 'react-router-dom'

const ItemForm = ({ item, handleSubmit, handleChange, cancelPath }) => (
  <form onSubmit={handleSubmit}>
    <label>Title</label>
    <input
      placeholder="A vetted item."
      value={item.title}
      name="title"
      onChange={handleChange}
    />

    <label>Link</label>
    <input
      placeholder="http://acoolitem.com"
      value={item.link}
      name="link"
      onChange={handleChange}
    />

    <button type="submit">Submit</button>
    <Link to={cancelPath}>
      <button>Cancel</button>
    </Link>
  </form>
)

export default ItemForm

Awesome! Now let's build our ItemEdit component:

components/routes/ItemEdit.jsx

import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import axios from 'axios'

import ItemForm from '../shared/ItemForm'
import Layout from '../shared/Layout'

class ItemEdit extends Component {
    constructor(props) {
        super(props)

        this.state = {
            item: {
                title: '',
                link: ''
            },
            updated: false
        }
    }

    async componentDidMount() {
        try {
            const response = await axios(`http://localhost:3001/api/items/${this.props.match.params.id}`)
            this.setState({ item: response.data.item })
        } catch (err) {
            console.error(err)
        }
    }

    handleChange = event => {
        const updatedField = { [event.target.name]: event.target.value }

        const editedItem = Object.assign(this.state.item, updatedField)

        this.setState({ item: editedItem })
    }

    handleSubmit = event => {
        event.preventDefault()

        axios({
            url: `http://localhost:3001/api/items/${this.props.match.params.id}`,
            method: 'PUT',
            data: { item: this.state.item }
        })
            .then(() => this.setState({ updated: true }))
            .catch(console.error)
    }

    render() {
        const { item, updated } = this.state
        const { handleChange, handleSubmit } = this

        if (updated) {
            return <Redirect to={`/items/${this.props.match.params.id}`} />
        }

        return (
            <Layout>
                <ItemForm
                    item={item}
                    handleChange={handleChange}
                    handleSubmit={handleSubmit}
                    cancelPath={`/items/${this.props.match.params.id}`}
                />
            </Layout>
        )
    }
}

export default ItemEdit

Let's test that. Open http://localhost:3001/items/1 and edit a field.

Nice! Now try delete. Bye.

Ok. We have one last CRUD action to complete in our react app - CREATE. Let's build the ItemCreat component and use our ItemForm shared component:

components/routes/ItemForm.jsx

import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import axios from 'axios'

import apiUrl from '../../apiConfig'
import ItemForm from '../shared/ItemForm'
import Layout from '../shared/Layout'

class ItemCreate extends Component {
    constructor(props) {
        super(props)

        this.state = {
            item: {
                title: '',
                link: ''
            },
            createdItem: null
        }
    }

    handleChange = event => {
        const updatedField = { [event.target.name]: event.target.value }

        const editedItem = Object.assign(this.state.item, updatedField)

        this.setState({ item: editedItem })
    }

    handleSubmit = event => {
        event.preventDefault()

        axios({
            url: `${apiUrl}/items`,
            method: 'POST',
            data: { item: this.state.item }
        })
            .then(res => this.setState({ createdItem: res.data.item }))
            .catch(console.error)
    }

    render() {
        const { handleChange, handleSubmit } = this
        const { createdItem, item } = this.state

        if (createdItem) {
            return <Redirect to={`/items`} />
        }

        return (
            <Layout>
                <ItemForm
                    item={item}
                    handleChange={handleChange}
                    handleSubmit={handleSubmit}
                    cancelPath="/"
                />
            </Layout>
        )
    }
}

export default ItemCreate

Great, test the create in your browser.

We now have full CRUD complete on the backend and on the frontend.

Success.

Bonus: Refactoring

Notice how we using the api url in multiple components. What would happen if we need to update the url, we would have to change it in several places. What if we could store the api url in one place and have it accessed from there? That way if we want to change the api url, we only change it in one place. We can do this!

Let's create an apiConfig component:

src/

touch apiConfig.jsx

src/apiConfig.jsx

let apiUrl
const apiUrls = {
  production: 'https://sei-items-api.herokuapp.com/api',
  development: 'http://localhost:3001/api'
}

if (window.location.hostname === 'localhost') {
  apiUrl = apiUrls.development
} else {
  apiUrl = apiUrls.production
}

export default apiUrl

Now replace all instances of http://localhost:3000/api in you Items, Item, ItemCreate, and ItemEdit components with ${apiUrl} and don't forget to import the apiConfig component: import apiUrl from '../../apiConfig'

Deploying to Heroku and Surge

  1. heroku create your-heroku-app-name
  2. heroku buildpacks:set heroku/nodejs
  3. heroku addons:create heroku-postgresql:hobby-dev --app=your-heroku-app-name
  4. git status
  5. git commit -am "add any pending changes"
  6. git push heroku master
  7. heroku run npx sequelize-cli db:migrate
  8. heroku run npx sequelize-cli db:seed:all

Having issues? Debug with the Heroku command heroku logs --tail to see what's happening on the Heroku server.

Test the endpoints :)

https://your-heroku-app-name.herokuapp.com/api/users

https://your-heroku-app-name.herokuapp.com/api/users/1

Cool the backend is now deployed! On to the frontend:

First you will have to replace anywhere inside your react app where you made an axios call to localhost:3000 to https://your-heroku-app-name.herokuapp.com - if you completed the bonus that means you will only have to update the apiConfig.js file with https://your-heroku-app-name.herokuapp.com as the value for the production key.

Now let's deploy the frontend:

cd client
npm run build
cd build
mv index.html 200.html
npx surge

Follow the prompts on Surge. Test the frontend routes once deployed. Getting errors? Check the browser dev tools. Check heroku logs --tail

Congrats! You built a full crud app with a backend and a frontend. You are now a fullstack developer!

Fist to Five

Feedback

Take a minute to give us feedback on this lesson so we can improve it!

express-api-react's People

Contributors

brunopgalvao avatar subtlesnow avatar

Watchers

James Cloos avatar michael ion avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.