|
| 1 | +--- |
| 2 | +description: Create a React frontend that submits data to backend that generates, stores and retrieves receipts. |
| 3 | +title_seo: Build a Frontend for Your Nitric Survey App |
| 4 | +tags: |
| 5 | + - React |
| 6 | + - Websites |
| 7 | + - API |
| 8 | +languages: |
| 9 | + - javascript |
| 10 | + - typescript |
| 11 | +published_at: 2025-04-03 |
| 12 | +updated_at: 2025-04-03 |
| 13 | +--- |
| 14 | + |
| 15 | +# Build a Frontend for Your Nitric Survey App with React |
| 16 | + |
| 17 | +In this guide, you’ll build a simple web application using React that connects to the survey backend we created with Nitric. |
| 18 | + |
| 19 | +## What You'll Build |
| 20 | + |
| 21 | +- A form to submit survey responses to the Nitric backend |
| 22 | +- A link to retrieve and view the submitted PDF receipt |
| 23 | + |
| 24 | +## Prerequisites |
| 25 | + |
| 26 | +- [Node.js](https://nodejs.org/en/download/) |
| 27 | +- The [Nitric survey backend project](./surveys-application) |
| 28 | + |
| 29 | +## Project Setup |
| 30 | + |
| 31 | +This guide assumes you already have the [survey backend project](./surveys-application). |
| 32 | + |
| 33 | +To add a website frontend, create a new Vite project: |
| 34 | + |
| 35 | +```bash |
| 36 | +npm create vite@latest main-website -- --template react-ts |
| 37 | +cd main-website |
| 38 | +npm install |
| 39 | +cd .. |
| 40 | +``` |
| 41 | + |
| 42 | +Next, configure your `nitric.yaml` to enable and configure the website: |
| 43 | + |
| 44 | +```yaml title:nitric.yaml |
| 45 | +name: survey-app |
| 46 | +services: |
| 47 | + - basedir: '' |
| 48 | + match: services/*.ts |
| 49 | + runtime: node |
| 50 | + start: npm run dev:services $SERVICE_PATH |
| 51 | +batch-services: [] |
| 52 | +websites: |
| 53 | + - basedir: ./main-website |
| 54 | + error: index.html |
| 55 | + build: |
| 56 | + command: npm run build |
| 57 | + output: dist |
| 58 | + dev: |
| 59 | + command: npm run dev -- --port 3000 |
| 60 | + url: http://localhost:3000 |
| 61 | +runtimes: |
| 62 | + node: |
| 63 | + dockerfile: ./node.dockerfile |
| 64 | + context: '' |
| 65 | + args: {} |
| 66 | +preview: |
| 67 | + - websites |
| 68 | +``` |
| 69 | +
|
| 70 | +## Connect to the API |
| 71 | +
|
| 72 | +Create a new folder `components` inside `main-website/src` and add the following file: |
| 73 | + |
| 74 | +This component collects the user's name, rating, and feedback, then submits the data to the Nitric API. |
| 75 | + |
| 76 | +```tsx title:main-website/src/components/SurveyForm.tsx |
| 77 | +import React, { useState } from 'react' |
| 78 | +
|
| 79 | +const SurveyForm = () => { |
| 80 | + const [formData, setFormData] = useState({ |
| 81 | + name: '', |
| 82 | + rating: 1, |
| 83 | + feedback: '', |
| 84 | + }) |
| 85 | + const [submissionId, setSubmissionId] = useState<string | null>(null) |
| 86 | +
|
| 87 | + const handleChange = ( |
| 88 | + e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, |
| 89 | + ) => { |
| 90 | + setFormData({ ...formData, [e.target.name]: e.target.value }) |
| 91 | + } |
| 92 | + ;<button |
| 93 | + onClick={async () => { |
| 94 | + try { |
| 95 | + const response = await fetch(`/api/forms/receipts/${submissionId}`) |
| 96 | + if (!response.ok) { |
| 97 | + throw new Error('Could not get receipt URL') |
| 98 | + } |
| 99 | + const { url } = await response.json() |
| 100 | + window.location.href = url // behaves like a 303 redirect |
| 101 | + } catch (err) { |
| 102 | + console.error(err) |
| 103 | + alert('Failed to load receipt') |
| 104 | + } |
| 105 | + }} |
| 106 | + > |
| 107 | + View Receipt |
| 108 | + </button> |
| 109 | + |
| 110 | + const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { |
| 111 | + e.preventDefault() |
| 112 | + try { |
| 113 | + const response = await fetch('/api/forms/forms/test-survey', { |
| 114 | + method: 'POST', |
| 115 | + headers: { |
| 116 | + 'Content-Type': 'application/json', |
| 117 | + }, |
| 118 | + body: JSON.stringify(formData), |
| 119 | + }) |
| 120 | + |
| 121 | + if (!response.ok) { |
| 122 | + throw new Error('Network response was not ok') |
| 123 | + } |
| 124 | + |
| 125 | + const data: { id: string } = await response.json() |
| 126 | + setSubmissionId(data.id) |
| 127 | + alert('Survey submitted successfully!') |
| 128 | + } catch (error) { |
| 129 | + console.error('Error submitting survey:', error) |
| 130 | + alert('Failed to submit survey.') |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + return ( |
| 135 | + <div> |
| 136 | + <h2>Survey</h2> |
| 137 | + <form onSubmit={handleSubmit}> |
| 138 | + <div> |
| 139 | + <label>Name:</label> |
| 140 | + <input |
| 141 | + type="text" |
| 142 | + name="name" |
| 143 | + value={formData.name} |
| 144 | + onChange={handleChange} |
| 145 | + required |
| 146 | + /> |
| 147 | + </div> |
| 148 | + <div> |
| 149 | + <label>Rating (1–5):</label> |
| 150 | + <input |
| 151 | + type="number" |
| 152 | + name="rating" |
| 153 | + value={formData.rating} |
| 154 | + onChange={handleChange} |
| 155 | + min="1" |
| 156 | + max="5" |
| 157 | + required |
| 158 | + /> |
| 159 | + </div> |
| 160 | + <div> |
| 161 | + <label>Feedback:</label> |
| 162 | + <textarea |
| 163 | + name="feedback" |
| 164 | + value={formData.feedback} |
| 165 | + onChange={handleChange} |
| 166 | + /> |
| 167 | + </div> |
| 168 | + <button type="submit">Submit</button> |
| 169 | + </form> |
| 170 | + |
| 171 | + {submissionId && ( |
| 172 | + <div> |
| 173 | + <p>Submission ID: {submissionId}</p> |
| 174 | + <button |
| 175 | + onClick={async () => { |
| 176 | + try { |
| 177 | + const response = await fetch( |
| 178 | + `/api/receipts/receipts/${submissionId}`, |
| 179 | + ) |
| 180 | + if (!response.ok) { |
| 181 | + throw new Error('Could not get receipt URL') |
| 182 | + } |
| 183 | + const url = await response.text() |
| 184 | + window.location.href = url |
| 185 | + } catch (err) { |
| 186 | + console.error(err) |
| 187 | + alert('Failed to load receipt') |
| 188 | + } |
| 189 | + }} |
| 190 | + > |
| 191 | + View Receipt |
| 192 | + </button> |
| 193 | + </div> |
| 194 | + )} |
| 195 | + </div> |
| 196 | + ) |
| 197 | +} |
| 198 | + |
| 199 | +export default SurveyForm |
| 200 | +``` |
| 201 | + |
| 202 | +### Add the component to your app |
| 203 | + |
| 204 | +Update the following file to use your new form component: |
| 205 | + |
| 206 | +```tsx title:main-website/src/App.tsx |
| 207 | +import React from 'react' |
| 208 | +import SurveyForm from './components/SurveyForm' |
| 209 | + |
| 210 | +function App() { |
| 211 | + return ( |
| 212 | + <div> |
| 213 | + <SurveyForm /> |
| 214 | + </div> |
| 215 | + ) |
| 216 | +} |
| 217 | + |
| 218 | +export default App |
| 219 | +``` |
| 220 | + |
| 221 | +## Run the Frontend Locally |
| 222 | + |
| 223 | +With your `nitric.yaml` configured correctly and the site added as a Nitric website, you can start both the frontend and backend using: |
| 224 | + |
| 225 | +```bash |
| 226 | +nitric start |
| 227 | +``` |
| 228 | + |
| 229 | +This will start the API and the website together, and serve your React frontend from [http://localhost:5000](http://localhost:5000) (confirm the port in the console output). |
| 230 | + |
| 231 | +To test it, visit [http://localhost:5000](http://localhost:5000) in your browser and submit a survey response. You should see a link to view the generated receipt hosted by the backend. |
| 232 | + |
| 233 | +## Conclusion |
| 234 | + |
| 235 | +Serving your React frontend and Nitric backend under the same origin, both locally and in the cloud, eliminates CORS issues and removes the need for headers, complex gateway config, or workarounds. |
0 commit comments