Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit ab482fb

Browse files
raksivjyecuschtjholmdavemooreuwsHomelessDinosaur
authored
docs: Add typescript survey guide (#732)
Co-authored-by: Jye Cusch <[email protected]> Co-authored-by: Tim Holm <[email protected]> Co-authored-by: David Moore <[email protected]> Co-authored-by: Ryan Cartwright <[email protected]>
1 parent 66be0f1 commit ab482fb

File tree

6 files changed

+639
-0
lines changed

6 files changed

+639
-0
lines changed

dictionary.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ EC2
237237
.mkv
238238
.jpg
239239
.pdf
240+
PDFs
241+
pdfs
242+
pdfkit
243+
html
240244
preflight
241245
lifecycle
242246
NodeJS
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
---
2+
description: Build a serverless backend for capturing and delivering survey submissions using Nitric and TypeScript.
3+
title_seo: Building a Survey Backend with Nitric and TypeScript
4+
tags:
5+
- API
6+
- Event
7+
- PDF
8+
languages:
9+
- typescript
10+
- javascript
11+
published_at: 2025-05-08
12+
updated_at: 2025-05-08
13+
---
14+
15+
# Building a Survey Backend with Nitric and TypeScript
16+
17+
This guide shows you how to build a backend for capturing, storing, and delivering survey responses using the Nitric framework. The application accepts survey submissions, generates PDF receipts, and delivers them asynchronously.
18+
19+
## API Overview
20+
21+
| **Method** | **Route** | **Description** |
22+
| ---------- | --------------- | ------------------------------------ |
23+
| `POST` | /forms/[formId] | Submit a response to a specific form |
24+
| `GET` | /receipts/[id] | Retrieve a generated receipt by ID |
25+
26+
Under the hood, the system also handles events via topics and handlers for generating PDFs and delivering them.
27+
28+
## Prerequisites
29+
30+
- [Node.js](https://nodejs.org/en/download/)
31+
- The [Nitric CLI](/get-started/installation)
32+
- _(optional)_ Your choice of an [AWS](https://aws.amazon.com), [GCP](https://cloud.google.com) or [Azure](https://azure.microsoft.com) account
33+
34+
## Project Setup
35+
36+
Create a new Nitric project using the TypeScript starter:
37+
38+
```bash
39+
nitric new surveys-backend ts-starter
40+
cd surveys-backend
41+
npm install
42+
```
43+
44+
You can now delete all files in the `services/` folder, we'll create new services in this guide.
45+
46+
## Add Runtime Type Safety
47+
48+
This project uses:
49+
50+
- [Zod](https://zod.dev/) to define and validate the structure of form submissions.
51+
- [pdfkit](https://pdfkit.org/) to generate PDFs from html templates.
52+
53+
### Install Zod
54+
55+
```bash
56+
npm install zod pdfkit
57+
```
58+
59+
### Define a schema for form submissions
60+
61+
Create a file at `form/schema.ts`:
62+
63+
```ts title:form/schema.ts
64+
import { z } from 'zod'
65+
66+
export const feedbackSchema = z.object({
67+
name: z.string(),
68+
rating: z.number().min(1).max(5),
69+
feedback: z.string().optional(),
70+
})
71+
72+
export const rsvpSchema = z.object({
73+
name: z.string(),
74+
attending: z.boolean(),
75+
guests: z.number().int().min(0),
76+
})
77+
78+
export const formSchemas: Record<string, z.ZodObject<any>> = {
79+
feedback: feedbackSchema,
80+
rsvp: rsvpSchema,
81+
}
82+
```
83+
84+
Feedback Schema: Validates that submissions include a name (string), a rating (number between 1 and 5), and optional feedback (string).
85+
RSVP Schema: Validates that submissions include a name (string), an attending status (boolean), and the number of guests (non-negative integer).
86+
87+
## Step 1: Define Resources
88+
89+
Create and configure the cloud resources your backend will use, such as storage buckets, queues, and key-value stores.
90+
91+
```ts title:resources/resources.ts
92+
import { bucket, kv, topic } from '@nitric/sdk'
93+
94+
export const output = bucket('receipts')
95+
export const submissions = kv('submissions')
96+
export const receipts = topic('form-submitted')
97+
```
98+
99+
## Step 2: Handle Submissions
100+
101+
Create an API route that validates and stores form submissions, then triggers further processing.
102+
103+
```ts title:services/forms.ts
104+
import { api } from '@nitric/sdk'
105+
import { submissions, receipts } from '../resources/resources'
106+
import { formSchemas } from '../form/schema'
107+
108+
const formApi = api('forms')
109+
const formSubmissions = submissions.allow('set')
110+
const submittableReceipts = receipts.allow('publish')
111+
112+
formApi.post('/forms/:formId', async (ctx) => {
113+
const formId = ctx.req.params.formId
114+
const schema = formSchemas[formId]
115+
116+
if (!schema) {
117+
ctx.res.status = 400
118+
ctx.res.json({ msg: `Unknown formId: ${formId}` })
119+
return
120+
}
121+
122+
const parsed = schema.safeParse(ctx.req.json())
123+
124+
if (!parsed.success) {
125+
ctx.res.status = 400
126+
ctx.res.json({ msg: 'Invalid submission', errors: parsed.error.format() })
127+
return
128+
}
129+
130+
const data = parsed.data
131+
const id = `${formId}-${Date.now()}`
132+
133+
await formSubmissions.set(id, data)
134+
await submittableReceipts.publish({ id, formId })
135+
136+
ctx.res.json({ msg: 'Submission received', id })
137+
})
138+
```
139+
140+
## Step 3: Generate PDF Receipts
141+
142+
Build a PDF receipt from the submission data using `pdfkit`.
143+
144+
```ts title:form/receipt.ts
145+
import PDFDocument from 'pdfkit'
146+
import { feedbackSchema, rsvpSchema } from './schema'
147+
import { z } from 'zod'
148+
149+
type FeedbackSubmission = z.infer<typeof feedbackSchema>
150+
type RsvpSubmission = z.infer<typeof rsvpSchema>
151+
152+
export const buildReceipt = async (
153+
data: any,
154+
formId: string,
155+
): Promise<Buffer> => {
156+
const receipt = new PDFDocument({ bufferPages: true })
157+
158+
const doneWriting = new Promise<Buffer>((resolve) => {
159+
const buffers: Uint8Array[] = []
160+
161+
receipt.on('data', buffers.push.bind(buffers))
162+
receipt.on('end', () => {
163+
const pdfData = Buffer.concat(buffers)
164+
resolve(pdfData)
165+
})
166+
167+
receipt.font('Times-Roman').fontSize(20).text('Survey - Receipt', 100, 100)
168+
receipt
169+
.font('Times-Roman')
170+
.fontSize(16)
171+
.text('Submission Details', 100, 150)
172+
173+
if (formId === 'feedback') {
174+
const { name, rating, feedback } = data as FeedbackSubmission
175+
176+
receipt
177+
.font('Times-Roman')
178+
.fontSize(12)
179+
.text(
180+
`Name: ${name}
181+
Rating: ${rating}
182+
Feedback: ${feedback || 'N/A'}`,
183+
100,
184+
175,
185+
)
186+
} else if (formId === 'rsvp') {
187+
const { name, attending, guests } = data as RsvpSubmission
188+
189+
receipt
190+
.font('Times-Roman')
191+
.fontSize(12)
192+
.text(
193+
`Name: ${name}
194+
Attending: ${attending ? 'Yes' : 'No'}
195+
Guests: ${guests}`,
196+
100,
197+
175,
198+
)
199+
} else {
200+
receipt
201+
.font('Times-Roman')
202+
.fontSize(12)
203+
.text('Unknown form type. No details available.', 100, 175)
204+
}
205+
206+
receipt.end()
207+
})
208+
209+
return await doneWriting
210+
}
211+
```
212+
213+
Listen for submitted events and generate a formatted PDF receipt from the stored data.
214+
215+
```ts title:services/pdfs.ts
216+
import { submissions, receipts, output } from '../resources/resources'
217+
import { buildReceipt } from '../form/receipt'
218+
219+
const formSubmissions = submissions.allow('get')
220+
const writeableOutput = output.allow('write')
221+
222+
receipts.subscribe(async (ctx) => {
223+
const { id, formId } = ctx.req.json()
224+
const submission = await formSubmissions.get(id)
225+
226+
if (!submission) {
227+
console.error(`No submission found for ID: ${id}`)
228+
return
229+
}
230+
231+
// Build the PDF buffer from the submission data
232+
const buffer = await buildReceipt(submission, formId)
233+
234+
// Store the PDF file in the bucket
235+
const file = writeableOutput.file(`${id}.pdf`)
236+
await file.write(buffer)
237+
238+
console.log(`Receipt stored for ${id}`)
239+
})
240+
```
241+
242+
This PDF output is fairly plain, you can enhance it further using layout templates or branding.
243+
244+
## Step 4: Delivery Logic
245+
246+
Simulate or perform delivery of the receipt (e.g. via email or other downstream systems).
247+
248+
```ts title:services/deliver.ts
249+
import { receipts } from '../resources/resources'
250+
251+
receipts.subscribe(async (ctx) => {
252+
const { id } = ctx.req.json()
253+
254+
// Simulate delivery or hook into a real email/SaaS integration
255+
console.log(`Delivering receipt for submission: ${id}`)
256+
})
257+
```
258+
259+
## Step 5: Retrieve Receipts
260+
261+
Create an endpoint that returns a download URL for the generated receipt file.
262+
263+
```ts title:services/receipts.ts
264+
import { api } from '@nitric/sdk'
265+
import { output } from '../resources/resources'
266+
267+
const receiptApi = api('receipts')
268+
const readableOutput = output.allow('read')
269+
270+
receiptApi.get('/receipts/:id', async (ctx) => {
271+
const id = ctx.req.params.id
272+
const file = readableOutput.file(`${id}.pdf`)
273+
const url = await file.getDownloadUrl()
274+
ctx.res.body = url
275+
})
276+
```
277+
278+
## Run and Test Locally
279+
280+
```bash
281+
nitric start
282+
```
283+
284+
### Submit a Survey
285+
286+
Use the Nitric dashboard to submit your survey data. In the path parameter, enter the formId you want to submit to (either "feedback" or "rsvp"). Then provide the survey data in the request body as JSON.
287+
288+
Form ID: `feedback`
289+
290+
```json
291+
{
292+
"name": "Jane",
293+
"rating": 5,
294+
"feedback": "Great experience!"
295+
}
296+
```
297+
298+
Form ID: `rsvp`
299+
300+
```json
301+
{
302+
"name": "Jane",
303+
"attending": true,
304+
"guests": 5
305+
}
306+
```
307+
308+
![Submit Form](/docs/images/guides/survey-application/submit.png)
309+
310+
You should see messages in your console for PDF generation and delivery.
311+
312+
```bash
313+
Delivering receipt for submission: rsvp-1746667675671
314+
Receipt stored for rsvp-1746667675671
315+
```
316+
317+
### Get a Receipt
318+
319+
Replace `<timestamp>` with the value returned from the submission.
320+
321+
![Get receipt](/docs/images/guides/survey-application/receipt.png)
322+
323+
Use the URL in the response to retrieve your PDF:
324+
325+
![Get PDF](/docs/images/guides/survey-application/pdf.png)
326+
327+
## Deploying to AWS
328+
329+
### Create your stack
330+
331+
Create an AWS stack called `aws-staging` for your staging environment.
332+
333+
```bash
334+
nitric stack new aws-staging aws
335+
```
336+
337+
Inside the stack file, ensure you set your `region`.
338+
339+
```yaml title:nitric.dev.yaml
340+
provider: nitric/aws@latest
341+
region: us-east-2
342+
```
343+
344+
### Deploy
345+
346+
Deploy to AWS using the `nitric up` command. Ensure you have set up your [AWS credentials](/providers/pulumi/aws#usage) correctly.
347+
348+
```bash
349+
nitric up
350+
```
351+
352+
### Tear down
353+
354+
To avoid unwanted costs of running your test app, you can tear down the stack using the `nitric down` command.
355+
356+
```bash
357+
nitric down
358+
```
359+
360+
## Summary
361+
362+
In this guide, you built a backend for handling survey submissions using Nitric and TypeScript:
363+
364+
- Created a REST API to accept form submissions
365+
- Validated user input using Zod
366+
- Stored submissions in a key-value store
367+
- Generated PDF receipts using pdfkit
368+
- Stored the receipts in cloud storage
369+
- Delivered them asynchronously via event topics
370+
- Exposed a URL endpoint to retrieve generated receipts
371+
372+
## What's next
373+
374+
- Build and deploy a [website for your project](./survey-website).

0 commit comments

Comments
 (0)