Api And Static Assets
A frontend that cannot talk to a backend is just a brochure -- here is how to wire up ESLint for JSX, proxy API requests, and serve images through Vite.
The JSX setup is done and the app renders in the browser. But it cannot talk to an API yet and the linter still does not understand JSX. Before adding any more features, both of those gaps need to close.
ESLint Needs a JSX Plugin
The ESLint config from the tooling post handles JavaScript. JSX adds a few extra rules that the base config does not cover, so install the official plugin:
npm install -D eslint-plugin-reactThen add it to eslint.config.mjs. The key sections to add are the plugin itself, the recommended rules, and one required setting that tells ESLint which React version you are using (without it, the version-gated rules cannot fire):
import reactPlugin from "eslint-plugin-react";
export default [
{
plugins: { react: reactPlugin },
rules: {
...reactPlugin.configs.recommended.rules,
"react/prop-types": 0,
},
settings: {
react: { version: "detect" },
},
},
// ... your other config objects
];react/prop-types is turned off because TypeScript handles that job better. Everything else in recommended is worth keeping.
htmlFor, Not for
One rule you will feel immediately: HTML for attributes become htmlFor in JSX. The same rename logic as class → className -- for is a reserved JavaScript keyword.
{/* wrong -- linter warning */}
<label for="size">Size</label>
{/* correct */}
<label htmlFor="size">Size</label>Self-Close Childless Tags
JSX requires every element with no children to be self-closed:
{/* wrong */}
<input type="text"></input>
{/* correct */}
<input type="text" />HTML is forgiving about this. JSX is not.
Uppercase for Components, Lowercase for HTML
JSX uses the case of the first letter to decide what something is. Lowercase means a native HTML element. Uppercase means a React component you wrote.
<div /> // native div
<Pizza /> // your Pizza componentIf you name a component with a lowercase letter and try to render it, React will try to render a native <pizza> element, which does not exist. The linter will warn you, but the rule is useful to have internalized anyway.
The API Server
The Padre Gino's project ships with a small Fastify server at api/index.js. Start it alongside Vite:
node api/index.jsThe server listens on localhost:3000 and exposes pizza data, types, and an order endpoint. The Vite dev server runs on a different port (usually 5173). If the browser tries to fetch /api/pizzas directly, it will hit port 5173 and get nothing because Vite does not know about that route.
Fixing the CORS Problem With a Proxy
Vite has a built-in proxy that solves this cleanly. Add it to vite.config.js:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://localhost:3000",
"/public": "http://localhost:3000",
},
},
}); ExpandVite dev server proxies /api and /public requests to the Fastify backend on port 3000
Now any fetch to /api/pizzas in the browser gets forwarded to http://localhost:3000/api/pizzas. The browser never sees the cross-origin request because Vite handles it server-to-server. In production, the real web server (Nginx, a CDN, whatever) takes over this proxy role.
The /public proxy matters for images. The API serves pizza images from localhost:3000/public/. With this rule in place, an <img src="/public/pepperoni.jpg" /> in the browser will resolve correctly through the Vite proxy.
Static Assets
CSS global styles live in style.css at the project root. Add a <link> to index.html -- not in a component, but in the HTML file that bootstraps the whole app:
<link rel="stylesheet" href="/style.css" />Vite serves files from the project root, so the path just works.
Images loaded dynamically (from the API) come through the proxy as described above. Images that are part of the app itself (a logo, a background) can go in the public/ folder that Vite creates, and you reference them with a root-relative path like /logo.svg.
With the API reachable and assets serving correctly, the next step is building the Order form -- a controlled component with a select and radio buttons that will eventually pull live data from that very API.
The Essentials
- eslint-plugin-react handles JSX-specific linting. The three rules that come up most:
htmlFornotfor, self-close childless tags, uppercase component names. react/prop-types: 0disables the prop-types rule. TypeScript handles that.settings.react.version: "detect"is required or version-gated rules will not fire.- Vite proxy routes
/apiand/publicto the backend during development. No CORS issues because the proxy is server-to-server. In production, configure the same routes on the real web server. - Global CSS lives in
index.htmlvia a<link>tag, not inside a component.
Further Reading and Watching
- Configuring eslint-plugin-react (GitHub) -- the canonical list of all available rules and configuration options
- Vite server.proxy docs -- official documentation for the proxy configuration, including how to rewrite paths and handle WebSocket proxying
- Why JSX uses className and htmlFor (Fireship) -- short explanation of why JSX diverges from HTML attribute names
Keep reading