Build Your First MERN Order Dashboard

Build an order website and learn how its page, server, and database connect.

Introduction

30 Second Summary

A new order can look safely saved on screen before disappearing the next time the system starts. That vanishing record can turn a smooth purchase into a customer-service problem.

In this project, you will build OrderFlow, a browser dashboard that creates orders through a React interface backed by an Express API. The project exposes process memory as temporary storage before MongoDB Atlas makes each order durable across an API restart.

What You'll Build

Your final OrderFlow demo shows an order surviving an API restart before Microsoft Edge displays it in the list again.

By the end of this project, you'll have:

  • An order intake dashboard where you create an order before seeing clear loading, success, or validation feedback.
  • A traceable MERN request path that sends JSON from the browser to Express before returning the saved order to React.
  • Visible persistence proof that keeps MongoDB-backed orders in the list after you restart Express.
  • Secret Mission: Add an instant customer-name filter that narrows the visible list without another API request.

Are there any prerequisites?

No prior MERN experience is required. The walkthrough covers every missing tool and account from setup onward.

Before We Start

Before any setup begins, this is your moment to lock in what OrderFlow lets someone do. You will also connect that goal to why orders need storage beyond a running API process.

Install Node.js and Start the React Client

OrderFlow needs a React client for the dashboard. It also needs an Express API for order requests.

Both applications depend on a JavaScript runtime. Explicit dependency manifests also make every installation reproducible.

In this step, get ready to:
  • Install the required Node.js runtime.
  • Create separate client and server packages.
  • Start the React client in Microsoft Edge.
Install and verify the Windows runtime and browser

Node.js runs JavaScript outside the browser. Its Windows installer also includes npm for installing project packages.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code and press Enter.
  • Select View from the Visual Studio Code menu.
  • Select Terminal to open the integrated terminal.
  • Check whether Node.js and npm are already available by running:
node -v
npm -v

What do these checks show?

The first command reports the installed Node.js version. The second reports the npm version supplied with the installer.

Choose the tab that matches the terminal output.

✔️ I see the required LTS version

Your terminal should show v26.13.0 for Node.js. npm should also print a version number.

That clears the first dependency hurdle. Your runtime can now install both OrderFlow packages.

ⓧ I see an older version

The existing runtime is below the version used by this project. Update it before installing any dependencies.

  • Visit the official Node.js download page.
  • Download the Windows installer labelled LTS.
  • Run the downloaded installer.
  • Keep the default npm option enabled.
  • Complete the installation.

The installer updates the runtime on your computer. A fresh terminal session picks up the new executable path.

  • Close the current terminal session.
  • Select Terminal from the Visual Studio Code menu.
  • Select New Terminal.
  • Verify the updated runtime by running:
node -v
npm -v

What confirms the update?

Node.js should now report v26.13.0. npm should print its own version on the next line.

Still seeing the older version?

Close every open Visual Studio Code window. Start Visual Studio Code again from Windows search before repeating the checks.

If the old version remains, help me check which Node.js installation Windows is using.

ⓧ Command not found

Windows cannot find Node.js yet. The LTS installer adds Node.js and npm together.

  • Visit the official Node.js download page.
  • Download the Windows installer labelled LTS.
  • Run the downloaded installer.
  • Keep the default npm option enabled.
  • Complete the installation.

The installer places both commands on the Windows executable path. Reopening the terminal makes that updated path available.

  • Close the current terminal session.
  • Select Terminal from the Visual Studio Code menu.
  • Select New Terminal.
  • Verify the installation by running:
node -v
npm -v

What confirms the installation?

Node.js should report v26.13.0. npm should print its own version on the next line.

Commands still unavailable?

Confirm that the Node.js installer completed successfully. Restart Visual Studio Code before repeating the checks.

If Windows still cannot find the commands, help me diagnose my Node.js installation.

Microsoft Edge provides the browser where you will run and inspect OrderFlow.

  • Press the Windows key to open Windows search.
  • Type Microsoft Edge and press Enter.

✔️ Microsoft Edge opens

Microsoft Edge is ready for the local OrderFlow page. Keep the browser open for the final check.

ⓧ Microsoft Edge is unavailable

Install Microsoft Edge before starting the client.

  • Visit the official Download Microsoft Edge page in another available browser.
  • Download the Windows version.
  • Run the downloaded installer.
  • Start Microsoft Edge from Windows search after installation.
Create explicit client and server packages

A package manifest records the scripts and dependency versions owned by one application. Separate manifests keep the browser package independent from the API package.

  • Return to Visual Studio Code.
  • Select File from the top menu.
  • Select Open Folder.
  • Select your Desktop in the folder dialog.
  • Select New Folder.
  • Enter orderflow as the folder name.
  • Select Select Folder.
  • Select Yes, I trust the authors if Workspace Trust appears.

Why use one workspace?

The orderflow workspace keeps the client and server together. Each side still owns a separate dependency manifest.

  • Select the orderflow workspace name in the Explorer.
  • Select New Folder.
  • Enter client and press Enter.
  • Select the orderflow workspace name again.
  • Select New Folder.
  • Enter server and press Enter.

You should now see separate client and server folders beneath orderflow.

  • Select the orderflow workspace name in the Explorer.
  • Select New File.
  • Enter .gitignore and press Enter.
  • Add the project exclusions by pasting:
node_modules/
dist/
config.env

What do these exclusions protect?

Installed packages and generated builds can be recreated from the manifests. The local environment file stays outside shared source control because it later holds the database connection string.

  • Save .gitignore.
  • Confirm .gitignore appears at the top level of the Explorer.

File missing from the workspace?

Confirm that you selected the orderflow workspace before creating the file. A selected child folder places the new file inside that folder.

If the file landed elsewhere, help me move my .gitignore file to the OrderFlow workspace.

  • Select the client folder in the Explorer.
  • Select New File.
  • Enter package.json and press Enter.
  • Define the client package by pasting:
{
  "name": "orderflow-client",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "19.3.1",
    "react-dom": "19.3.1"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "6.1.2",
    "vite": "8.3.5"
  }
}

What does the client manifest control?

  • The scripts define how npm starts and builds the browser application.
  • The dependencies pin the React packages used by the interface.
  • The development dependencies pin Vite and its React plugin.
  • Save client/package.json.
  • Confirm package.json appears beneath client in the Explorer.

Seeing JSON warnings?

Check that every property name uses double quotes. Confirm that the final dependency entries have no trailing commas.

If the warning remains, help me compare my client package manifest.

  • Select the server folder in the Explorer.
  • Select New File.
  • Enter package.json and press Enter.
  • Define the API package by pasting:
{
  "name": "orderflow-server",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "node --env-file=config.env server"
  },
  "dependencies": {
    "express": "5.3.0",
    "mongoose": "9.11.4"
  }
}

What does the server manifest control?

  • The development script loads values from config.env before starting the API.
  • The dependencies pin Express for HTTP handling.
  • Mongoose is pinned now so the later database steps use the same installation.
  • Save server/package.json.
  • Confirm package.json appears beneath server in the Explorer.

Server manifest marked invalid?

Check the braces around the scripts and dependencies objects. Confirm that the dependency entries are separated by one comma.

If the warning remains, help me repair my server package manifest.

  • Select the server folder in the Explorer.
  • Select New File.
  • Enter config.env and press Enter.
  • Set the local API port by pasting:
PORT=5050

Why define the port here?

The environment file keeps runtime configuration outside the application code. The API uses port 5050 throughout this project.

  • Save server/config.env.
  • Confirm config.env appears beneath server in the Explorer.
  • Select the server folder in the Explorer.
  • Select New File.
  • Enter config.env.example and press Enter.
  • Add the safe configuration template by pasting:
MONGODB_URI=your-mongodb-atlas-connection-string-here
PORT=5050

Why keep an example file?

The example records every required environment setting without containing a live credential. It can remain in shared source control while config.env stays private.

  • Save server/config.env.example.
  • Confirm both configuration files appear beneath server in the Explorer.

The manifests now describe each package. npm can use them to create separate installed dependency folders.

  • Right-click the client folder in the Explorer.
  • Select Open in Integrated Terminal.
  • Install the client dependencies by running:
npm install

What does this installation create?

npm reads client/package.json and downloads the pinned client packages. It stores them inside client/node_modules.

  • Confirm node_modules appears beneath client in the Explorer.

Client installation failed?

Confirm that the terminal path ends with orderflow\client. Check that client/package.json is saved without JSON warnings.

If npm still fails, help me debug my client dependency installation.

  • Right-click the server folder in the Explorer.
  • Select Open in Integrated Terminal.
  • Install the server dependencies by running:
npm install

What does the server installation create?

npm reads server/package.json and downloads the pinned API packages. It stores them inside server/node_modules.

  • Confirm node_modules appears beneath server in the Explorer.

Server installation failed?

Confirm that the terminal path ends with orderflow\server. Check that server/package.json is saved without JSON warnings.

If npm still fails, help me debug my server dependency installation.

Render the first React page

Vite serves the client during development. React then mounts the App component inside the browser document.

  • Select the client folder in the Explorer.
  • Select New File.
  • Enter index.html and press Enter.
  • Create the browser document by pasting:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>OrderFlow</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

What does the document provide?

  • The root element gives React a browser container.
  • The module script loads the client entry file.
  • The page title identifies the app as OrderFlow.
  • Save client/index.html.
  • Confirm index.html appears beneath client in the Explorer.

HTML file in the wrong folder?

Confirm that index.html sits directly inside client. It should sit beside package.json.

If the structure differs, help me correct my client file locations.

  • Select the client folder in the Explorer.
  • Select New File.
  • Enter vite.config.js and press Enter.
  • Enable the React plugin by pasting:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})

What does this configuration do?

The configuration loads Vite's React plugin. That plugin prepares React source files for the development server.

  • Save client/vite.config.js.
  • Confirm vite.config.js appears beside client/package.json.

Configuration marked invalid?

Confirm that both import names match the installed package names. Check the closing braces around defineConfig().

If the warning remains, help me inspect my Vite configuration.

  • Select the client folder in the Explorer.
  • Select New Folder.
  • Enter src and press Enter.
  • Select the new src folder.
  • Select New File.
  • Enter main.jsx and press Enter.
  • Create the React entry point by pasting:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

How does React enter the page?

  • The imports load React's checks and browser rendering API.
  • The createRoot() call attaches React to the HTML container.
  • The rendered tree places App inside StrictMode.
  • Save client/src/main.jsx.
  • Confirm main.jsx appears beneath client/src.

Entry file showing import errors?

Confirm that the client dependency installation completed. Check that the file is named main.jsx inside client/src.

If imports still fail, help me debug my React entry file.

  • Select the src folder in the Explorer.
  • Select New File.
  • Enter App.jsx and press Enter.
  • Create the starter component by pasting:
export default function App() {
  return (
    <main className="shell">
      <h1>OrderFlow</h1>
      <p>Build a persistent MERN order dashboard.</p>
    </main>
  )
}

What does the component render?

The App function returns the starter page content. The shell class gives the stylesheet one container to size and position.

  • Save client/src/App.jsx.
  • Confirm App.jsx appears beside main.jsx.

Component showing a syntax warning?

Check that the returned markup sits inside parentheses. Confirm that every opening element has a matching closing element.

If the warning remains, help me repair my starter App component.

  • Select the src folder in the Explorer.
  • Select New File.
  • Enter index.css and press Enter.
  • Add the starter page styles by pasting:
:root {
  font-family: Inter, system-ui, sans-serif;
  color: #172033;
  background: #eef2f7;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

.shell {
  width: min(920px, calc(100% - 32px));
  margin: 48px auto;
}

What do these styles establish?

  • The root rules set the page typeface and colours.
  • The universal rule keeps element sizing predictable.
  • The body rules give the page a stable minimum size.
  • The shell rule keeps the content readable on wide and narrow screens.
  • Save client/src/index.css.
  • Confirm index.css appears beside App.jsx.

Stylesheet showing a warning?

Confirm that each selector has matching braces. Check the punctuation in the min() and calc() values.

If the warning remains, help me inspect my starter stylesheet.

Before you start the client, what heading do you expect React to place inside the browser page?

  • Right-click the client folder in the Explorer.
  • Select Open in Integrated Terminal.
  • Start the Vite development server by running:
npm run dev

What does the development command do?

npm runs the dev script from the client manifest. Vite starts a local server and prints a local URL in the terminal.

The process keeps this terminal busy while the client is running. You can stop it later with Ctrl+C.

  • Select the printed local URL in the terminal.
  • Choose Microsoft Edge if Windows asks which browser to use.

You should see the OrderFlow heading above the dashboard description. The pale background and centred content confirm that the stylesheet also loaded.

  • Return to client/src/App.jsx in Visual Studio Code.
  • Replace the heading text OrderFlow with Build a persistent MERN order dashboard..
  • Save client/src/App.jsx.

Microsoft Edge should update immediately to show the longer heading. That visible change proves the development server is watching the client files.

  • Replace the heading text with OrderFlow again.
  • Save client/src/App.jsx.

The browser should return to the OrderFlow heading. Your saved file now matches the project checkpoint.

Page not loading in Microsoft Edge?

Confirm that the Vite process is still running in the client terminal. Use the exact local URL printed by that process.

If the terminal reports an import error, check the filenames inside client/src against the import paths in main.jsx.

If the page still fails, help me debug my OrderFlow starter page.

✔️ Awesome, I've got everything!

Your manifests and starter files are saved. Keep the client development server running for the next step.

ⓧ I'd like to double check the full code

Compare each saved file with the complete checkpoint below.

node_modules/
dist/
config.env

What to compare

Confirm that all three excluded paths appear on separate lines.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>OrderFlow</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

What to compare

Confirm that the root container and module script match this document.

{
  "name": "orderflow-client",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "19.3.1",
    "react-dom": "19.3.1"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "6.1.2",
    "vite": "8.3.5"
  }
}

What to compare

Confirm that the client scripts and four dependency pins match exactly.

export default function App() {
  return (
    <main className="shell">
      <h1>OrderFlow</h1>
      <p>Build a persistent MERN order dashboard.</p>
    </main>
  )
}

What to compare

Confirm that the component contains the original OrderFlow heading and description.

:root {
  font-family: Inter, system-ui, sans-serif;
  color: #172033;
  background: #eef2f7;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

.shell {
  width: min(920px, calc(100% - 32px));
  margin: 48px auto;
}

What to compare

Confirm that every selector and starter style matches this file.

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

What to compare

Confirm that all four imports and the rendered component tree match.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})

What to compare

Confirm that the React plugin appears once in the Vite configuration.

PORT=5050

What to compare

Confirm that the private environment file contains the local API port.

MONGODB_URI=your-mongodb-atlas-connection-string-here
PORT=5050

What to compare

Confirm that the example contains the plain connection-string placeholder and API port.

{
  "name": "orderflow-server",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "node --env-file=config.env server"
  },
  "dependencies": {
    "express": "5.3.0",
    "mongoose": "9.11.4"
  }
}

What to compare

Confirm that the server script and two dependency pins match exactly.

Your React client is running from a reproducible workspace. Next, you will connect that page to its first Express API response.

Connect React to an Express API

Your React starter now renders in Microsoft Edge. A dashboard that only renders its own markup cannot retrieve server data.

You will build a small Express API. React will fetch its seeded order through the Vite development proxy.

In this step, get ready to:
  • Serve a seeded order from an Express endpoint.
  • Proxy relative API requests through Vite.
  • Render the API response from React state.
Create a readable Express boundary

The API creates an HTTP boundary between the browser interface and server-owned data. A seeded in-memory order gives you a predictable response to test.

  • In Visual Studio Code, expand the server folder in the Explorer sidebar.
  • Create server.js inside the server folder by using the Explorer file controls.
  • Build the initial API process by pasting this into server.js:
import express from 'express'

const app = express()
const port = Number(process.env.PORT || 5050)
const orders = [
  {
    _id: 'seed-order-1',
    customerName: 'Ada Lovelace',
    item: 'Mechanical keyboard',
    quantity: 1,
    createdAt: new Date().toISOString(),
  },
]

app.use(express.json())

app.listen(port, () => {
  console.log(`OrderFlow API listening on http://localhost:${port}`)
})

How Does the API Boundary Work?

  • The Express application receives requests on the port loaded from config.env.
  • The orders array holds one seeded order in process memory.
  • The express.json() middleware prepares the API to parse JSON request bodies.
  • The listener keeps the API process running on http://localhost:5050.
  • Save server.js.
  • In the Explorer sidebar, right-click the server folder.
  • Choose the context-menu option that opens the folder in an integrated terminal.
  • Start the API process by running:
npm run dev

What Does This Command Do?

The server package script loads config.env before starting server.js. This makes PORT available through process.env.

You should see OrderFlow API listening on http://localhost:5050 in the terminal. Your API process is now ready to receive requests.

API Process Not Starting?

  • Confirm the terminal is using the server folder.
  • Confirm config.env contains PORT=5050.
  • Confirm the earlier dependency installation created server/node_modules.

Ask for help to debug my Express startup problem.

The process is listening now. The next addition gives the browser a route that can return the seeded order.

  • In server.js, find the line beginning with app.listen.
  • Add the orders route immediately above that line by pasting:
app.get('/api/orders', (request, response) => {
  response.json(orders)
})

What Does This Route Do?

The GET /api/orders route handles read requests for the order collection. response.json(orders) sends the in-memory array back to the browser.

  • Save server.js.
  • Stop the running API process with Ctrl+C.
  • Restart the API with the new route by running:
npm run dev

Why Restart the API?

The server script runs directly through Node.js. Restarting the process loads the new GET /api/orders route into Express.

  • Enter http://localhost:5050/api/orders in the Microsoft Edge address bar.

You should see an array containing the customer Ada Lovelace and the item Mechanical keyboard. This proves that Express can return the seeded order.

✔️ Awesome, I've got everything!

Your server.js file now starts Express and serves the seeded order.

ⓧ I'd like to double check the full code

  • Compare your complete server.js file with this reference:
import express from 'express'

const app = express()
const port = Number(process.env.PORT || 5050)
const orders = [
  {
    _id: 'seed-order-1',
    customerName: 'Ada Lovelace',
    item: 'Mechanical keyboard',
    quantity: 1,
    createdAt: new Date().toISOString(),
  },
]

app.use(express.json())

app.get('/api/orders', (request, response) => {
  response.json(orders)
})

app.listen(port, () => {
  console.log(`OrderFlow API listening on http://localhost:${port}`)
})
Proxy API requests during development

A development proxy forwards matching browser requests to another local process. React can request /api/orders without knowing the API port.

  • Select vite.config.js inside the client folder.
  • Replace the file contents with this Vite configuration:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://localhost:5050',
    },
  },
})

How Does the Proxy Work?

  • The server.proxy configuration watches requests beginning with /api.
  • Vite forwards matching requests to http://localhost:5050.
  • The browser code keeps a relative request path that does not depend on the API port.
  • Save vite.config.js.
  • Switch back to the client terminal from earlier.
  • Stop the running client process with Ctrl+C.
  • Restart Vite with the proxy configuration by running:
npm run dev

Why Restart Vite?

Vite reads vite.config.js when the development server starts. Restarting activates the new proxy rule.

You should see the local client URL in the terminal again. The OrderFlow starter page should still load at that URL.

Client Not Loading After the Restart?

  • Confirm the client terminal is using the client folder.
  • Check that every opening brace in vite.config.js has a matching closing brace.
  • Keep the API terminal running while the client uses the proxy.

Ask for help to debug my Vite proxy configuration.

✔️ Awesome, I've got everything!

Your Vite development server now forwards relative API requests to Express.

ⓧ I'd like to double check the full code

  • Compare your complete vite.config.js file with this reference:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://localhost:5050',
    },
  },
})
Synchronize React with the API

React needs state for the response and its loading status. An effect starts the request after the component mounts.

  • Select App.jsx inside client/src.
  • Find the line beginning with export default function App().
  • Replace that line with this import and state setup:
import { useEffect, useState } from 'react'

export default function App() {
  const [orders, setOrders] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState('')

What Does This State Track?

  • The orders state stores the array returned by Express.
  • The loading state controls the message shown while the request is in progress.
  • The error state stores a useful message when the request fails.
  • Find the existing return ( line in App.jsx.
  • Add the data-loading effect immediately above that line by pasting:
  useEffect(() => {
    let ignore = false

    async function loadOrders() {
      try {
        const response = await fetch('/api/orders')
        if (!response.ok) throw new Error('Could not load orders.')
        const data = await response.json()
        if (!ignore) setOrders(data)
      } catch (loadError) {
        if (!ignore) setError(loadError.message)
      } finally {
        if (!ignore) setLoading(false)
      }
    }

    loadOrders()
    return () => { ignore = true }
  }, [])

How Does the Effect Stay Safe?

  • The loadOrders() function requests the relative /api/orders path.
  • The response check turns an unsuccessful request into a readable error.
  • The ignore flag prevents a completed request from updating an inactive effect.
  • The finally block ends the loading state after success or failure.
  • Save App.jsx.
  • Open Microsoft Edge DevTools by pressing F12.
  • Select the Network panel.
  • Refresh the OrderFlow page.

You should see a successful request for /api/orders in the Network panel. This proves the effect can reach Express through Vite.

  • In App.jsx, locate the existing return block.
  • Replace that return block with this interface:
  return (
    <main className="shell">
      <h1>OrderFlow</h1>
      {loading && <p>Loading orders...</p>}
      {error && <p>{error}</p>}
      {!loading && orders.map((order) => <p key={order._id}>{order.customerName}: {order.item}</p>)}
    </main>
  )
}

How Does React Choose What to Display?

  • The loading condition displays a message while the request is active.
  • The error condition displays the request failure message.
  • The order mapping creates one paragraph for each returned order.
  • The _id value gives each rendered order a stable React key.
  • Save App.jsx.

Before you refresh, which customer and item do you expect React to render from the API response?

  • Refresh the OrderFlow page in Microsoft Edge.
  • Confirm the Network panel records the proxied /api/orders request.

You should see Ada Lovelace: Mechanical keyboard below the OrderFlow heading. You should also see the successful request in the Network panel.

  • Trace the request aloud in this order: React sends the request. Vite proxies it. Express returns the data. React stores the result in state.

Seeded Order Not Appearing?

  • Confirm both the client terminal and the server terminal still show running processes.
  • Confirm App.jsx fetches the relative path /api/orders.
  • Confirm the Network panel shows a request for /api/orders.

Ask for help to debug my missing seeded order.

✔️ Awesome, I've got everything!

Your React client now loads the seeded order through the complete request path.

ⓧ I'd like to double check the full code

  • Compare your complete App.jsx file with this reference:
import { useEffect, useState } from 'react'

export default function App() {
  const [orders, setOrders] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState('')

  useEffect(() => {
    let ignore = false

    async function loadOrders() {
      try {
        const response = await fetch('/api/orders')
        if (!response.ok) throw new Error('Could not load orders.')
        const data = await response.json()
        if (!ignore) setOrders(data)
      } catch (loadError) {
        if (!ignore) setError(loadError.message)
      } finally {
        if (!ignore) setLoading(false)
      }
    }

    loadOrders()
    return () => { ignore = true }
  }, [])

  return (
    <main className="shell">
      <h1>OrderFlow</h1>
      {loading && <p>Loading orders...</p>}
      {error && <p>{error}</p>}
      {!loading && orders.map((order) => <p key={order._id}>{order.customerName}: {order.item}</p>)}
    </main>
  )
}

That is your first complete MERN request path working. Next, you will submit a new order and discover why process memory cannot preserve it.

Submit an Order and Expose the Memory Problem

Your React dashboard now displays a seeded order returned by Express. That working read path does not show how a new order travels back to the API.

A controlled form keeps each input synchronized with React state. An HTTP POST request carries that state to the API as JSON.

After the write path works, you will restart only Express to test whether an order outlives the process that created it.

In this step, get ready to:
  • Build a controlled order form with customer, item, and quantity fields.
  • Send new orders to the Express API.
  • Restart Express to test whether submitted orders survive.
Prepare controlled form state

The form object keeps related input values together. The submitting value lets the interface react while a request is in progress.

  • In client/src/App.jsx, replace the import and state setup at the top of the file with this code:
import { useEffect, useState } from 'react'

const emptyForm = {
  customerName: '',
  item: '',
  quantity: 1,
}

export default function App() {
  const [orders, setOrders] = useState([])
  const [form, setForm] = useState(emptyForm)
  const [loading, setLoading] = useState(true)
  const [submitting, setSubmitting] = useState(false)
  const [error, setError] = useState('')

What does this state hold?

  • The emptyForm object provides the initial values that the form can return to after a successful submission.
  • The form state keeps the three input values in one object.
  • The submitting state tracks whether the API request is still running.
  • In client/src/App.jsx, find the }, []) line that closes the effect.
  • Add this function below the effect:
  function updateField(event) {
    const { name, value } = event.target
    setForm((current) => ({ ...current, [name]: value }))
  }

How does the field update work?

Each input supplies its own name with its current value. The computed property updates only that named field.

The spread syntax copies the other fields into a new object. This keeps the state update immutable.

  • Save client/src/App.jsx.
  • Refresh OrderFlow in Microsoft Edge.

You should still see the seeded Ada Lovelace order. This confirms that the new state compiles without breaking the existing read path.

Did the seeded order disappear?

Check that emptyForm sits above App(). Confirm that every state declaration remains inside App().

Compare the braces around updateField() with the snippet. A missing brace can prevent the client from compiling.

help me debug the controlled form state

Add the order submission path

The API needs a write boundary for new orders. The route builds one order from the request body before placing it at the front of the in-memory array.

  • In server/server.js, find the closing brace of the GET /api/orders route.
  • Add this POST route below it:
app.post('/api/orders', (request, response) => {
  const order = {
    _id: crypto.randomUUID(),
    customerName: request.body.customerName,
    item: request.body.item,
    quantity: request.body.quantity,
    createdAt: new Date().toISOString(),
  }
  orders.unshift(order)
  response.status(201).json(order)
})

What does this route do?

  • The route reads the three submitted fields from request.body.
  • The crypto.randomUUID() call assigns a unique identifier to the new order.
  • The timestamp records when the API created the order.
  • The array puts the newest order first before the API returns it with status 201.
  • Save server/server.js.
  • Press Ctrl+C in the server terminal to stop the existing Express process.
  • Restart the API from the server folder by running:
npm run dev

What does this command start?

The development script starts server.js with the values from config.env. The restarted process now includes the POST route.

You should see OrderFlow API listening on http://localhost:5050 in the server terminal.

Did the API fail to restart?

Confirm that the terminal is inside the server folder. Check that the POST route closes before app.listen() begins.

help me debug the POST route

✔️ Awesome, I've got everything!

Your Express API now accepts new orders in memory. Double check that server/server.js is saved.

ⓧ I'd like to double check the full code

Compare your complete server/server.js file with this reference:

import express from 'express'

const app = express()
const port = Number(process.env.PORT || 5050)
const orders = [
  {
    _id: 'seed-order-1',
    customerName: 'Ada Lovelace',
    item: 'Mechanical keyboard',
    quantity: 1,
    createdAt: new Date().toISOString(),
  },
]

app.use(express.json())

app.get('/api/orders', (request, response) => {
  response.json(orders)
})

app.post('/api/orders', (request, response) => {
  const order = {
    _id: crypto.randomUUID(),
    customerName: request.body.customerName,
    item: request.body.item,
    quantity: request.body.quantity,
    createdAt: new Date().toISOString(),
  }
  orders.unshift(order)
  response.status(201).json(order)
})

app.listen(port, () => {
  console.log(`OrderFlow API listening on http://localhost:${port}`)
})

The client now needs to send its form state to that route. A successful response becomes the newest item in the existing orders state.

  • In client/src/App.jsx, add this function below updateField():
  async function submitOrder(event) {
    event.preventDefault()
    setSubmitting(true)
    setError('')
    try {
      const response = await fetch('/api/orders', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ customerName: form.customerName, item: form.item, quantity: Number(form.quantity) }),
      })
      const data = await response.json()
      if (!response.ok) throw new Error(data.message || 'Order failed.')
      setOrders((current) => [data, ...current])
      setForm(emptyForm)
    } catch (submitError) {
      setError(submitError.message)
    } finally {
      setSubmitting(false)
    }
  }

How does submission work?

  • The function prevents the browser from performing its default form navigation.
  • The request converts the form object into JSON before sending it to /api/orders.
  • A successful response is prepended to the current order list.
  • The form resets only after the API accepts the order.
  • In client/src/App.jsx, find the existing return ( block.
  • Replace that block through the final function brace with this interface:
  return (
    <main className="shell">
      <h1>OrderFlow</h1>
      <form onSubmit={submitOrder}>
        <input name="customerName" value={form.customerName} onChange={updateField} placeholder="Customer" />
        <input name="item" value={form.item} onChange={updateField} placeholder="Item" />
        <input name="quantity" type="number" value={form.quantity} onChange={updateField} />
        <button disabled={submitting}>{submitting ? 'Saving...' : 'Create order'}</button>
      </form>
      {error && <p>{error}</p>}
      {loading && <p>Loading orders...</p>}
      {!loading && orders.map((order) => <p key={order._id}>{order.customerName}: {order.item}</p>)}
    </main>
  )
}

How is the form controlled?

Each input reads its value from form. Every change passes through the same updateField() function.

The button becomes disabled while submission is active. Its label changes to show the request state.

  • Save client/src/App.jsx.
  • Switch back to Microsoft Edge.
  • Refresh OrderFlow.

You should see three inputs above the seeded order. You should also see a Create order button.

  • Enter Grace Hopper in the Customer input.
  • Enter Monitor stand in the Item input.
  • Enter 2 in the Quantity input.
  • Click Create order.

You should see Grace Hopper at the top of the list without a full page reload. The three inputs should reset to their initial values.

You have a complete write path now. The browser sends form state to Express before React displays the returned order.

Did the new order fail to appear?

Confirm that both the client terminal and server terminal are still running. Check that the form uses onSubmit={submitOrder}.

Open Microsoft Edge DevTools to confirm the POST request reaches /api/orders. A failed request means the client did not receive a created order to prepend.

help me trace the failed order submission

✔️ Awesome, I've got everything!

Great. Your saved client/src/App.jsx file now controls the form and submits orders to the API.

ⓧ I'd like to double check the full code

Compare your complete client/src/App.jsx file with this reference:

import { useEffect, useState } from 'react'

const emptyForm = {
  customerName: '',
  item: '',
  quantity: 1,
}

export default function App() {
  const [orders, setOrders] = useState([])
  const [form, setForm] = useState(emptyForm)
  const [loading, setLoading] = useState(true)
  const [submitting, setSubmitting] = useState(false)
  const [error, setError] = useState('')

  useEffect(() => {
    let ignore = false
    async function loadOrders() {
      try {
        const response = await fetch('/api/orders')
        if (!response.ok) throw new Error('Could not load orders.')
        const data = await response.json()
        if (!ignore) setOrders(data)
      } catch (loadError) {
        if (!ignore) setError(loadError.message)
      } finally {
        if (!ignore) setLoading(false)
      }
    }
    loadOrders()
    return () => { ignore = true }
  }, [])

  function updateField(event) {
    const { name, value } = event.target
    setForm((current) => ({ ...current, [name]: value }))
  }

  async function submitOrder(event) {
    event.preventDefault()
    setSubmitting(true)
    setError('')
    try {
      const response = await fetch('/api/orders', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ customerName: form.customerName, item: form.item, quantity: Number(form.quantity) }),
      })
      const data = await response.json()
      if (!response.ok) throw new Error(data.message || 'Order failed.')
      setOrders((current) => [data, ...current])
      setForm(emptyForm)
    } catch (submitError) {
      setError(submitError.message)
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <main className="shell">
      <h1>OrderFlow</h1>
      <form onSubmit={submitOrder}>
        <input name="customerName" value={form.customerName} onChange={updateField} placeholder="Customer" />
        <input name="item" value={form.item} onChange={updateField} placeholder="Item" />
        <input name="quantity" type="number" value={form.quantity} onChange={updateField} />
        <button disabled={submitting}>{submitting ? 'Saving...' : 'Create order'}</button>
      </form>
      {error && <p>{error}</p>}
      {loading && <p>Loading orders...</p>}
      {!loading && orders.map((order) => <p key={order._id}>{order.customerName}: {order.item}</p>)}
    </main>
  )
}
Restart Express and test durability

The submitted order now appears in the browser and inside the server's array. Restarting only the API tests the lifetime of that data.

Before you restart Express, do you think the Grace Hopper order will still appear after the process starts again?

  • Press Ctrl+C in the server terminal to stop only Express.
  • Restart Express from the server folder by running:
npm run dev

What changed during the restart?

The previous Node.js process ended. The new process executes server.js from the beginning with a newly created orders array.

  • Switch back to Microsoft Edge.
  • Refresh OrderFlow.

You should see only the seeded Ada Lovelace order. The Grace Hopper order has disappeared.

Why did the order disappear?

The browser completed the request correctly. Express also created and returned the order correctly.

The array existed only in process memory. Stopping the process destroyed that array before the restarted server created a fresh copy.

That disappearing order proves exactly where the current architecture falls short. You now have direct evidence that a successful API response does not guarantee durable storage.

Did the submitted order remain?

Confirm that you stopped the server terminal instead of the client terminal. The server terminal prints the API listening message on port 5050.

Refresh the browser after the new Express process starts. React must issue a fresh GET request before the reset array becomes visible.

help me verify the Express restart

Your complete request path works, but its data disappears with the API process. Next, you will connect Express to MongoDB Atlas so orders can live outside server memory.

Connect the API to MongoDB Atlas

Your last restart proved that the complete write path works. The order vanished because process memory belongs to one Express process.

Now you will connect the API to MongoDB Atlas. Mongoose will establish the database boundary before the next step moves orders out of the temporary array.

In this step, get ready to:
  • Create a Free Atlas cluster.
  • Secure the cluster with a database user plus a current-IP access-list entry.
  • Make the API wait for MongoDB before accepting requests.
Create the Free Atlas cluster

MongoDB Atlas is a managed cloud database service. Its Free cluster costs $0/hour.

The tier includes 512MB of storage. No credit card is required.

  • Return to Microsoft Edge from earlier.
  • Open the official MongoDB Atlas setup guide.
  • Create a MongoDB Atlas account through the registration flow.
  • Create one cluster using the Free tier.

Atlas may spend several minutes provisioning the cluster. That delay is normal.

  • Wait until Atlas shows that the cluster is ready.

Good progress. Your Free cluster is ready to become OrderFlow's external data store.

This next part creates a database credential. The generated password can access your data.

  • Select Add Your Current IP Address in the connection workflow.
  • Select Add IP Address to save the access-list entry.
  • Enter your Atlas database username as the separate database username.
  • Select Autogenerate Secure Password.
  • Store the generated password in your password manager.
  • Select Create Database User.

How does Atlas protect the cluster?

  • The IP access list controls which networks can connect to the cluster.
  • The database user authenticates your API separately from your Atlas account.
  • Both checks must pass before Mongoose can establish a connection.
Protect the Atlas connection string

An environment variable keeps deployment-specific configuration outside your application code. The API reads the private connection string from server/config.env when it starts.

  • Select Choose Your Connection Method in Atlas.
  • Select Drivers.
  • Choose Node.js as the driver.
  • Copy the SRV connection string beginning with mongodb+srv://.
  • Switch back to Visual Studio Code.
  • Select server/config.env in the Explorer sidebar.
  • Replace the file contents with this configuration:
MONGODB_URI=your-mongodb-atlas-connection-string-here
PORT=5050

What does this configuration hold?

  • MONGODB_URI stores the private address plus database credentials that Mongoose uses.
  • PORT keeps the local API on port 5050.
  • The plain connection-string placeholder prevents a live credential from entering shared source control.
  • Replace your-mongodb-atlas-connection-string-here with the SRV connection string copied from Atlas.
  • Replace the password placeholder inside the connection string with the generated database password.
  • Save server/config.env.
  • Confirm that server/config.env contains exactly two configuration lines.
  • Select server/config.env.example in the Explorer sidebar.
  • Confirm that server/config.env.example still contains only the plain connection-string placeholder.
  • Select .gitignore in the Explorer sidebar.
  • Confirm that config.env remains listed in .gitignore.
Start Express after MongoDB connects

A database connection is an application boundary. Starting Express after that connection succeeds prevents the API from accepting requests without its required data store.

  • Select server/server.js in the Explorer sidebar.
  • Find the existing import line:
import express from 'express'

What does the current import provide?

This import creates the Express application. The server does not yet have access to the MongoDB connection API.

  • Replace the existing import with these two imports:
import express from 'express'
import mongoose from 'mongoose'

What does the new import add?

The Mongoose import provides mongoose.connect(). The server uses it to open the Atlas connection before listening for requests.

  • Scroll to the bottom of server/server.js.
  • Find the current startup block:
app.listen(port, () => {
  console.log(`OrderFlow API listening on http://localhost:${port}`)
})

How does the server currently start?

The current block starts listening immediately. It does not confirm that an external database is available.

  • Replace the current startup block with this connection-aware startup function:
async function startServer() {
  if (!process.env.MONGODB_URI) {
    throw new Error('MONGODB_URI is missing from config.env')
  }

  await mongoose.connect(process.env.MONGODB_URI, { dbName: 'orderflow' })
  console.log('MongoDB connected')

  app.listen(port, () => {
    console.log(`OrderFlow API listening on http://localhost:${port}`)
  })
}

startServer().catch((error) => {
  console.error('Server startup failed:', error.message)
  process.exit(1)
})

What does this startup function do?

  • The environment check stops startup when MONGODB_URI is absent.
  • The awaited mongoose.connect() call opens the orderflow database.
  • The app.listen() call runs only after MongoDB accepts the connection.
  • The final catch block reports startup failures before exiting the process.
  • Save server/server.js.

✔️ Awesome, I've got everything!

Your server now waits for Atlas before it begins listening. Double check that server/server.js is saved.

ⓧ I'd like to double check the full code

Compare your complete server/server.js file with this reference:

import express from 'express'
import mongoose from 'mongoose'

const app = express()
const port = Number(process.env.PORT || 5050)
const orders = [
  {
    _id: 'seed-order-1',
    customerName: 'Ada Lovelace',
    item: 'Mechanical keyboard',
    quantity: 1,
    createdAt: new Date().toISOString(),
  },
]

app.use(express.json())

app.get('/api/orders', (request, response) => {
  response.json(orders)
})

app.post('/api/orders', (request, response) => {
  const order = {
    _id: crypto.randomUUID(),
    customerName: request.body.customerName,
    item: request.body.item,
    quantity: request.body.quantity,
    createdAt: new Date().toISOString(),
  }
  orders.unshift(order)
  response.status(201).json(order)
})

async function startServer() {
  if (!process.env.MONGODB_URI) {
    throw new Error('MONGODB_URI is missing from config.env')
  }

  await mongoose.connect(process.env.MONGODB_URI, { dbName: 'orderflow' })
  console.log('MongoDB connected')

  app.listen(port, () => {
    console.log(`OrderFlow API listening on http://localhost:${port}`)
  })
}

startServer().catch((error) => {
  console.error('Server startup failed:', error.message)
  process.exit(1)
})

Why keep the temporary array?

This step isolates the database connection boundary. The GET and POST routes still use the in-memory orders array.

The next step replaces those array operations with validated Mongoose reads and writes.

  • Switch back to the existing server terminal.
  • Press Ctrl+C to stop the current Express process.

Before you start the server, which message do you expect to appear first: the database connection message or the API listening message?

  • Start the API from the server folder by running:
npm run dev

What does this command start?

The npm script loads config.env before executing server.js. The startup function then attempts the Atlas connection.

You will see MongoDB connected first. You will then see OrderFlow API listening on http://localhost:5050.

That is the connection boundary proven: OrderFlow refuses to listen until Atlas is available.

Server did not reach the listening message?

  • Confirm that your current IP address remains in the Atlas project access list.
  • Confirm that the Atlas database user matches your Atlas database username.
  • Re-enter the generated database password inside the private SRV connection string.
  • Confirm that MONGODB_URI is spelled exactly in server/config.env.

Ask for help with debugging the Atlas connection.

Your API now waits for Atlas before accepting requests. Next, you will replace the temporary array with validated Mongoose documents that survive restarts.

Persist and Validate Orders with Mongoose

Your Express API now reaches MongoDB Atlas before accepting requests. Its order routes still depend on an array that disappears with the Node.js process.

Mongoose gives each order a validated schema before MongoDB stores it. This step moves order storage into database documents.

In this step, get ready to:
  • Define the document rules for every order.
  • Replace the temporary array with MongoDB reads and writes.
  • Return useful responses when an order fails validation.
Define the order document contract

An order model gives every stored document the same shape. Its validators stop incomplete orders before they reach the database.

  • Switch back to Visual Studio Code from the earlier steps.
  • Select the server folder in the left file sidebar.
  • Create a folder named models inside server using the sidebar's folder control.
  • Select the new models folder.
  • Create Order.js inside server/models using the sidebar's file control.
  • Paste the order model into server/models/Order.js using this code:
import mongoose from 'mongoose'

const orderSchema = new mongoose.Schema(
  {
    customerName: {
      type: String,
      required: [true, 'Customer name is required'],
    },
    item: {
      type: String,
      required: [true, 'Item is required'],
    },
    quantity: {
      type: Number,
      required: [true, 'Quantity is required'],
      min: [1, 'Quantity must be at least 1'],
    },
  },
  { timestamps: true },
)

export const Order = mongoose.model('Order', orderSchema)

What does this model enforce?

  • The string validators require every order to include a customer name plus an item.
  • The numeric validator rejects any quantity below 1.
  • The timestamps option adds createdAt plus updatedAt to each document.
  • The exported Order model gives the API one interface for validated reads and writes.
  • Save server/models/Order.js.
  • Confirm the left file sidebar shows Order.js inside server/models.

Cannot find the model file?

Check that Order.js sits inside server/models. A file created inside client cannot be imported by the API using the required relative path.

Ask for help with the file location if the sidebar structure differs: help me place Order.js in the correct OrderFlow folder.

✔️ Awesome, I've got everything!

Your saved model now defines the rules that every database order must satisfy.

ⓧ I'd like to double check the full code

Compare your complete server/models/Order.js file with this reference:

import mongoose from 'mongoose'

const orderSchema = new mongoose.Schema(
  {
    customerName: {
      type: String,
      required: [true, 'Customer name is required'],
    },
    item: {
      type: String,
      required: [true, 'Item is required'],
    },
    quantity: {
      type: Number,
      required: [true, 'Quantity is required'],
      min: [1, 'Quantity must be at least 1'],
    },
  },
  { timestamps: true },
)

export const Order = mongoose.model('Order', orderSchema)

What should match?

Confirm the three fields use the same validator messages shown here. The export must also use the exact model name Order.

Replace process memory with database operations

The model exists as a separate module. The API must import it before either route can use MongoDB as its order source.

  • In server/server.js, replace the two existing import lines with this import group:
import express from 'express'
import mongoose from 'mongoose'
import { Order } from './models/Order.js'

What does this import add?

The relative path loads the exported Order model from the new file. The route handlers can now call its database methods.

  • Save server/server.js.
  • Stop the API process in the server terminal from earlier by pressing Ctrl+C.
  • Restart the API from the existing server terminal by running:
npm run dev

What does this restart check?

A clean startup proves that server.js can resolve the new model module. The process still waits for MongoDB before it starts listening.

You should see MongoDB connected before OrderFlow API listening on http://localhost:5050.

Server fails after the import?

Check that the import uses ./models/Order.js. Confirm the filename uses a capital O.

If the paths match but startup still fails, ask for targeted help: help me debug the Order model import.

The read route still returns the temporary array. Replacing it makes MongoDB Atlas the source for every page load.

  • In server/server.js, replace the existing GET /api/orders route with this database-backed route:
app.get('/api/orders', async (request, response) => {
  const orders = await Order.find({}).sort({ createdAt: -1 })
  response.json(orders)
})

How does the database read work?

  • Order.find({}) retrieves every stored order document.
  • sort({ createdAt: -1 }) places the newest order first.
  • response.json(orders) sends the resulting documents to the React client.
  • Save server/server.js.
  • Stop the running API by pressing Ctrl+C.
  • Load the new read route by running:
npm run dev

What changed in this process?

This restart loads the new asynchronous GET handler. The handler now waits for MongoDB results before responding.

  • Return to Microsoft Edge from the earlier steps.
  • Refresh the OrderFlow page.

You should see No orders yet. because the hardcoded seeded order no longer comes from the GET route. This empty list proves the browser is reading the database collection.

The write route still builds a plain object. Replacing it sends new orders through the schema before MongoDB stores them.

  • In server/server.js, replace the existing POST /api/orders route with this database-backed route:
app.post('/api/orders', async (request, response) => {
  const order = await Order.create({
    customerName: request.body.customerName,
    item: request.body.item,
    quantity: request.body.quantity,
  })

  response.status(201).json(order)
})

How does the database write work?

  • Order.create() runs the supplied values through the schema validators before saving.
  • The explicit field mapping limits the write to customerName, item, plus quantity.
  • The 201 response returns the saved document with its generated ID plus timestamps.
  • Save server/server.js.
  • Stop the running API by pressing Ctrl+C.
  • Load the new write route by running:
npm run dev

What is active now?

Both API routes now use the Order model. The old array remains declared for the moment, but neither route reads it nor writes to it.

  • Enter Ada Lovelace in the Customer field.
  • Enter Mechanical keyboard in the Item field.
  • Enter 1 in the Quantity field.
  • Click Create order.

You should see the new order at the top of the list. Its returned ID plus creation time came from the saved MongoDB document.

  • Delete the entire temporary orders declaration that contains seed-order-1 from server/server.js.
  • Save server/server.js.
  • Stop the running API by pressing Ctrl+C.
  • Restart the API without the temporary array by running:
npm run dev

What does removing the array prove?

The new process contains no seeded data structure. Any order returned after this restart must come from MongoDB Atlas.

  • Refresh OrderFlow in Microsoft Edge.

You should still see the order you created. The API process was replaced, but the database document remained.

Return useful validation responses

A rejected document reaches Express as a Mongoose validation error. Error-handling middleware can translate its field messages into JSON that the existing React form already displays.

  • In server/server.js, add this middleware after the POST route and before startServer():
app.use((error, request, response, next) => {
  if (response.headersSent) {
    return next(error)
  }

  if (error.name === 'ValidationError') {
    return response.status(400).json({
      message: 'Invalid order.',
      errors: Object.values(error.errors).map((detail) => detail.message),
    })
  }

  console.error(error)
  return response.status(500).json({ message: 'Something went wrong.' })
})

How does the error boundary work?

  • The headers check hands an error back to Express when a response has already started.
  • A ValidationError becomes a 400 response containing each failed field message.
  • An unexpected failure becomes a generic 500 response.
  • The existing React submit handler joins the returned validation messages for the visible error paragraph.
  • Save server/server.js.

✔️ Awesome, I've got everything!

Your server now reads from MongoDB, writes validated documents, plus converts failures into useful responses.

ⓧ I'd like to double check the full code

Compare your complete server/server.js file with this reference:

import express from 'express'
import mongoose from 'mongoose'
import { Order } from './models/Order.js'

const app = express()
const port = Number(process.env.PORT || 5050)

app.use(express.json())

app.get('/api/orders', async (request, response) => {
  const orders = await Order.find({}).sort({ createdAt: -1 })
  response.json(orders)
})

app.post('/api/orders', async (request, response) => {
  const order = await Order.create({
    customerName: request.body.customerName,
    item: request.body.item,
    quantity: request.body.quantity,
  })

  response.status(201).json(order)
})

app.use((error, request, response, next) => {
  if (response.headersSent) {
    return next(error)
  }

  if (error.name === 'ValidationError') {
    return response.status(400).json({
      message: 'Invalid order.',
      errors: Object.values(error.errors).map((detail) => detail.message),
    })
  }

  console.error(error)
  return response.status(500).json({ message: 'Something went wrong.' })
})

async function startServer() {
  if (!process.env.MONGODB_URI) {
    throw new Error('MONGODB_URI is missing from config.env')
  }

  await mongoose.connect(process.env.MONGODB_URI, { dbName: 'orderflow' })
  console.log('MongoDB connected')

  app.listen(port, () => {
    console.log(`OrderFlow API listening on http://localhost:${port}`)
  })
}

startServer().catch((error) => {
  console.error('Server startup failed:', error.message)
  process.exit(1)
})

What should match?

Confirm the model import appears above the application setup. The error middleware must appear after both routes.

  • Stop the running API by pressing Ctrl+C.
  • Load the completed server by running:
npm run dev

What does the completed server load?

The fresh process loads the model-backed routes plus the error middleware. It begins listening only after the Atlas connection succeeds.

You should again see MongoDB connected before the listening message.

  • Enter Ada Lovelace in the Customer field.
  • Enter Mechanical keyboard in the Item field.
  • Enter 1 in the Quantity field.
  • Click Create order.

You should see the new order at the top of the dashboard without a full page reload.

Before you restart the API, do you expect this newest order to survive after the current process ends?

  • Stop the API process by pressing Ctrl+C.
  • Start a fresh API process by running:
npm run dev

What does this restart test?

The restart discards every value owned by the previous Node.js process. Refreshing next checks whether the database can reconstruct the list independently.

  • Refresh the OrderFlow page in Microsoft Edge.

You should see the newest order still in the list. Its survival proves that MongoDB Atlas is now the only order source of truth.

Before submitting an invalid quantity, predict whether the API will add the order or return the schema's message.

  • Enter Ada Lovelace in the Customer field.
  • Enter Mechanical keyboard in the Item field.
  • Enter 0 in the Quantity field.
  • Click Create order.

You should see Quantity must be at least 1 beside the form. The invalid order should remain absent from the list.

Validation message does not appear?

  • Confirm the quantity validator uses min: [1, 'Quantity must be at least 1'].
  • Confirm the error middleware sits below the POST route.
  • Restart the API after saving both server files.

For a response that still fails silently, ask for help tracing the error: help me trace the Mongoose validation response.

That is the persistence boundary solved. Valid orders now survive restarts while invalid orders stop at the schema.

Secret mission

Filter Orders Without Another API Call

Turn the existing order collection into an instant customer search. You will derive a filtered view from React state while keeping the API, MongoDB documents, and source order array unchanged.

Clean Up Your Resources

Clean Up Your Resources

Keeping the local orderflow workspace has no ongoing cost. Your MongoDB Atlas Free cluster costs $0/hour within Free-tier limits, so decide whether to keep everything available, pause your work, or delete each resource.

Resources you used:

  • The local Vite client development process, if it is still running.
  • The local Express API process, if it is still running.
  • The orderflow workspace on your Desktop. This includes the private server/config.env file.
  • The MongoDB Atlas Free cluster that stores your orders.
  • The Atlas database user used by the API.
  • The current-IP access-list entry used by the Atlas project.

Keep everything running

No action is needed. Choose this if you are still actively testing OrderFlow or plan to continue soon.

  • Leave the Vite client process running if you are still testing the dashboard.
  • Leave the Express API process running if you are still creating orders.
  • Keep the Atlas Free cluster available for later use.
  • Keep server/config.env private because it contains your database credential.

Pause - I'll come back to this later

Shut down the local processes to free their ports and memory. Your project files and stored orders remain available for your return.

  • Press Ctrl+C in the Vite client terminal to stop its process.
  • Press Ctrl+C in the Express API terminal to stop its process.
  • Leave the Atlas Free cluster idle.

Atlas automatically pauses a Free cluster after 30 days of inactivity with zero connections. You can resume the cluster when you return.

Delete - I don't want to use this again

Remove all project resources and start fresh. This cleanup permanently removes your saved orders and private connection file.

The confirmation screen deserves a careful look. Atlas provides it as a final checkpoint before your orders are removed.

  • Press Ctrl+C in the Vite client terminal if its process is still running.
  • Press Ctrl+C in the Express API terminal if its process is still running.
  • Open the database deployment view in your Atlas project to locate the Free cluster.
  • Delete the Free cluster from its cluster actions menu.
  • Complete the on-screen deletion confirmation.

The cluster and its saved orders are now removed. The project-level access entries still remain.

  • Open the database-access settings in the same Atlas project to locate the database user.
  • Remove the database user you created for OrderFlow.
  • Open the project IP access-list settings to locate the current-IP entry.
  • Remove the current-IP access-list entry.

That is the cloud cleanup complete. The cluster and its access entries are gone.

  • Press the Windows key to open Windows search.
  • Search for File Explorer.
  • Open the File Explorer result.
  • Select Desktop in the sidebar.
  • Select the orderflow folder.
  • Delete the selected orderflow folder.
  • Use Windows search to open the Recycle Bin.
  • Delete the orderflow folder permanently from the Recycle Bin.

You are all clear. Your local code and private server/config.env credential are now removed from this machine.

Nice Work!

Nice Work!

You did it! You built OrderFlow, a persistent MERN dashboard that carries validated orders from a React form through an Express API into MongoDB Atlas.

You've learned how to:

  • Create a controlled React interface that submits JSON orders. Show loading states, successful submissions, and validation errors directly in the dashboard.
  • Build Express GET and POST endpoints that move orders through an HTTP request path. Restart the API to prove that process memory loses data when the server stops.
  • Use Mongoose schema validation to reject invalid orders. Store accepted orders in MongoDB Atlas so they survive API restarts.
  • Secret Mission: Derive an instant customer-name filter from existing React state. Preserve the original order array without making another API request.

Ready to quiz yourself?