SlideShare a Scribd company logo
1 of 61
Download to read offline
2022-12-07 1
META_SLIDE!
loige
loige.link/pizzaparty
2
OUR MISSION TODAY:
LET'S BUILD AN INVITE-ONLY WEBSITE!
loige 3
15 SECONDS DEMO ⏱
loige 4
loige5
(SELF-IMPOSED) REQUIREMENTS
Iterate quickly
Simple to host, maintain and update
Lightweight backend
Non-techy people can easily access the data
💸Cheap (or even FREE) hosting!
loige 6
LET ME INTRODUCE MYSELF FIRST...
👋I'm Luciano ( 🍕🍝 )
Senior Architect @ fourTheorem (Dublin )
nodejsdp.link
📔Co-Author of Node.js Design Patterns 👉
Let's connect!
(blog)
(twitter)
(twitch)
(github)
loige.co
@loige
loige
lmammino 7
ALWAYS RE-IMAGINING
WE ARE A PIONEERING TECHNOLOGY CONSULTANCY FOCUSED ON AWS AND SERVERLESS
| |
Accelerated Serverless AI as a Service Platform Modernisation
loige
✉Reach out to us at
😇We are always looking for talent:
hello@fourTheorem.com
fth.link/careers
8
📒AGENDA
Choosing the tech stack
The data flow
Using Airtable as a database
Creating APIs with Next.js and Vercel
Creating custom React Hooks
Using user interaction to update the data
Security considerations
loige 9
TECH STACK 🥞
loige 10
MAKING A NEXT.JS PRIVATE
Every guest should see something different
People without an invite code should not be able to access any
content
loige 11
loige
example.com?code=secret
example.com?code=secret
✅
❌
example.com?code=secret
❌
Access denied
example.com?code=secret
Hello, Micky
you are invited...
Load React SPA Code validation View invite (or error)
12
STEP 1.
LET'S ORGANIZE THE DATA IN AIRTABLE
loige 13
MANAGING DATA
Invite codes are UUIDs
Every record contains the information for every guest (name, etc)
loige 14
AIRTABLE LINGO
loige
Base (project) Table
Records
Fields
15
STEP 2.
NEXT.JS SCAFFOLDING AND RETRIEVING INVITES
loige 16
NEW NEXT.JS PROJECTS
loige
npx create-next-app@12.2 --typescript --use-npm
(used Next.js 12.2)
17
INVITE TYPE
loige
export interface Invite {
code: string,
name: string,
favouriteColor: string,
weapon: string,
coming?: boolean,
}
18
AIRTABLE SDK
loige
npm i --save airtable
export AIRTABLE_API_KEY="put your api key here"
export AIRTABLE_BASE_ID="put your base id here"
19
loige 20
loige 21
loige 22
// utils/airtable.ts
import Airtable from 'airtable'
import { Invite } from '../types/invite'
if (!process.env.AIRTABLE_API_KEY) {
throw new Error('AIRTABLE_API_KEY is not set')
}
if (!process.env.AIRTABLE_BASE_ID) {
throw new Error('AIRTABLE_BASE_ID is not set')
}
const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY })
const base = airtable.base(process.env.AIRTABLE_BASE_ID)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// utils/airtable.ts
1
2
import Airtable from 'airtable'
3
import { Invite } from '../types/invite'
4
5
if (!process.env.AIRTABLE_API_KEY) {
6
throw new Error('AIRTABLE_API_KEY is not set')
7
}
8
if (!process.env.AIRTABLE_BASE_ID) {
9
throw new Error('AIRTABLE_BASE_ID is not set')
10
}
11
12
const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY })
13
const base = airtable.base(process.env.AIRTABLE_BASE_ID)
14
import Airtable from 'airtable'
import { Invite } from '../types/invite'
// utils/airtable.ts
1
2
3
4
5
if (!process.env.AIRTABLE_API_KEY) {
6
throw new Error('AIRTABLE_API_KEY is not set')
7
}
8
if (!process.env.AIRTABLE_BASE_ID) {
9
throw new Error('AIRTABLE_BASE_ID is not set')
10
}
11
12
const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY })
13
const base = airtable.base(process.env.AIRTABLE_BASE_ID)
14
if (!process.env.AIRTABLE_API_KEY) {
throw new Error('AIRTABLE_API_KEY is not set')
}
if (!process.env.AIRTABLE_BASE_ID) {
throw new Error('AIRTABLE_BASE_ID is not set')
}
// utils/airtable.ts
1
2
import Airtable from 'airtable'
3
import { Invite } from '../types/invite'
4
5
6
7
8
9
10
11
12
const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY })
13
const base = airtable.base(process.env.AIRTABLE_BASE_ID)
14
const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY })
const base = airtable.base(process.env.AIRTABLE_BASE_ID)
// utils/airtable.ts
1
2
import Airtable from 'airtable'
3
import { Invite } from '../types/invite'
4
5
if (!process.env.AIRTABLE_API_KEY) {
6
throw new Error('AIRTABLE_API_KEY is not set')
7
}
8
if (!process.env.AIRTABLE_BASE_ID) {
9
throw new Error('AIRTABLE_BASE_ID is not set')
10
}
11
12
13
14
// utils/airtable.ts
import Airtable from 'airtable'
import { Invite } from '../types/invite'
if (!process.env.AIRTABLE_API_KEY) {
throw new Error('AIRTABLE_API_KEY is not set')
}
if (!process.env.AIRTABLE_BASE_ID) {
throw new Error('AIRTABLE_BASE_ID is not set')
}
const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY })
const base = airtable.base(process.env.AIRTABLE_BASE_ID)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
loige
23
export function getInvite (inviteCode: string): Promise<Invite> {
return new Promise((resolve, reject) => {
base('invites')
.select({
filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape
maxRecords: 1
})
.firstPage((err, records) => {
if (err) {
console.error(err)
return reject(err)
}
if (!records || records.length === 0) {
return reject(new Error('Invite not found'))
}
resolve({
code: String(records[0].fields.invite),
name: String(records[0].fields.name),
favouriteColor: String(records[0].fields.favouriteColor),
weapon: String(records[0].fields.weapon),
coming: typeof records[0].fields.coming === 'undefined'
? undefined
: records[0].fields.coming === 'yes'
})
})
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export function getInvite (inviteCode: string): Promise<Invite> {
}
1
return new Promise((resolve, reject) => {
2
base('invites')
3
.select({
4
filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape
5
maxRecords: 1
6
})
7
.firstPage((err, records) => {
8
if (err) {
9
console.error(err)
10
return reject(err)
11
}
12
13
if (!records || records.length === 0) {
14
return reject(new Error('Invite not found'))
15
}
16
17
resolve({
18
code: String(records[0].fields.invite),
19
name: String(records[0].fields.name),
20
favouriteColor: String(records[0].fields.favouriteColor),
21
weapon: String(records[0].fields.weapon),
22
coming: typeof records[0].fields.coming === 'undefined'
23
? undefined
24
: records[0].fields.coming === 'yes'
25
})
26
})
27
})
28
29
return new Promise((resolve, reject) => {
})
export function getInvite (inviteCode: string): Promise<Invite> {
1
2
base('invites')
3
.select({
4
filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape
5
maxRecords: 1
6
})
7
.firstPage((err, records) => {
8
if (err) {
9
console.error(err)
10
return reject(err)
11
}
12
13
if (!records || records.length === 0) {
14
return reject(new Error('Invite not found'))
15
}
16
17
resolve({
18
code: String(records[0].fields.invite),
19
name: String(records[0].fields.name),
20
favouriteColor: String(records[0].fields.favouriteColor),
21
weapon: String(records[0].fields.weapon),
22
coming: typeof records[0].fields.coming === 'undefined'
23
? undefined
24
: records[0].fields.coming === 'yes'
25
})
26
})
27
28
}
29
base('invites')
export function getInvite (inviteCode: string): Promise<Invite> {
1
return new Promise((resolve, reject) => {
2
3
.select({
4
filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape
5
maxRecords: 1
6
})
7
.firstPage((err, records) => {
8
if (err) {
9
console.error(err)
10
return reject(err)
11
}
12
13
if (!records || records.length === 0) {
14
return reject(new Error('Invite not found'))
15
}
16
17
resolve({
18
code: String(records[0].fields.invite),
19
name: String(records[0].fields.name),
20
favouriteColor: String(records[0].fields.favouriteColor),
21
weapon: String(records[0].fields.weapon),
22
coming: typeof records[0].fields.coming === 'undefined'
23
? undefined
24
: records[0].fields.coming === 'yes'
25
})
26
})
27
})
28
}
29
.select({
filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape
maxRecords: 1
})
export function getInvite (inviteCode: string): Promise<Invite> {
1
return new Promise((resolve, reject) => {
2
base('invites')
3
4
5
6
7
.firstPage((err, records) => {
8
if (err) {
9
console.error(err)
10
return reject(err)
11
}
12
13
if (!records || records.length === 0) {
14
return reject(new Error('Invite not found'))
15
}
16
17
resolve({
18
code: String(records[0].fields.invite),
19
name: String(records[0].fields.name),
20
favouriteColor: String(records[0].fields.favouriteColor),
21
weapon: String(records[0].fields.weapon),
22
coming: typeof records[0].fields.coming === 'undefined'
23
? undefined
24
: records[0].fields.coming === 'yes'
25
})
26
})
27
})
28
}
29
.firstPage((err, records) => {
})
export function getInvite (inviteCode: string): Promise<Invite> {
1
return new Promise((resolve, reject) => {
2
base('invites')
3
.select({
4
filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape
5
maxRecords: 1
6
})
7
8
if (err) {
9
console.error(err)
10
return reject(err)
11
}
12
13
if (!records || records.length === 0) {
14
return reject(new Error('Invite not found'))
15
}
16
17
resolve({
18
code: String(records[0].fields.invite),
19
name: String(records[0].fields.name),
20
favouriteColor: String(records[0].fields.favouriteColor),
21
weapon: String(records[0].fields.weapon),
22
coming: typeof records[0].fields.coming === 'undefined'
23
? undefined
24
: records[0].fields.coming === 'yes'
25
})
26
27
})
28
}
29
if (err) {
console.error(err)
return reject(err)
}
if (!records || records.length === 0) {
return reject(new Error('Invite not found'))
}
export function getInvite (inviteCode: string): Promise<Invite> {
1
return new Promise((resolve, reject) => {
2
base('invites')
3
.select({
4
filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape
5
maxRecords: 1
6
})
7
.firstPage((err, records) => {
8
9
10
11
12
13
14
15
16
17
resolve({
18
code: String(records[0].fields.invite),
19
name: String(records[0].fields.name),
20
favouriteColor: String(records[0].fields.favouriteColor),
21
weapon: String(records[0].fields.weapon),
22
coming: typeof records[0].fields.coming === 'undefined'
23
? undefined
24
: records[0].fields.coming === 'yes'
25
})
26
})
27
})
28
}
29
resolve({
code: String(records[0].fields.invite),
name: String(records[0].fields.name),
favouriteColor: String(records[0].fields.favouriteColor),
weapon: String(records[0].fields.weapon),
coming: typeof records[0].fields.coming === 'undefined'
? undefined
: records[0].fields.coming === 'yes'
})
export function getInvite (inviteCode: string): Promise<Invite> {
1
return new Promise((resolve, reject) => {
2
base('invites')
3
.select({
4
filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape
5
maxRecords: 1
6
})
7
.firstPage((err, records) => {
8
if (err) {
9
console.error(err)
10
return reject(err)
11
}
12
13
if (!records || records.length === 0) {
14
return reject(new Error('Invite not found'))
15
}
16
17
18
19
20
21
22
23
24
25
26
})
27
})
28
}
29
export function getInvite (inviteCode: string): Promise<Invite> {
return new Promise((resolve, reject) => {
base('invites')
.select({
filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape
maxRecords: 1
})
.firstPage((err, records) => {
if (err) {
console.error(err)
return reject(err)
}
if (!records || records.length === 0) {
return reject(new Error('Invite not found'))
}
resolve({
code: String(records[0].fields.invite),
name: String(records[0].fields.name),
favouriteColor: String(records[0].fields.favouriteColor),
weapon: String(records[0].fields.weapon),
coming: typeof records[0].fields.coming === 'undefined'
? undefined
: records[0].fields.coming === 'yes'
})
})
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
loige
24
STEP 3.
NEXT.JS INVITE API
loige 25
// pages/api/hello.ts -> <host>/api/hello
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler (
req: NextApiRequest,
res: NextApiResponse<{ message: string }>
) {
return res.status(200).json({ message: 'Hello World' })
}
1
2
3
4
5
6
7
8
9
10
// pages/api/hello.ts -> <host>/api/hello
1
2
import type { NextApiRequest, NextApiResponse } from 'next'
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<{ message: string }>
7
) {
8
return res.status(200).json({ message: 'Hello World' })
9
}
10
import type { NextApiRequest, NextApiResponse } from 'next'
// pages/api/hello.ts -> <host>/api/hello
1
2
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<{ message: string }>
7
) {
8
return res.status(200).json({ message: 'Hello World' })
9
}
10
export default async function handler (
req: NextApiRequest,
res: NextApiResponse<{ message: string }>
) {
}
// pages/api/hello.ts -> <host>/api/hello
1
2
import type { NextApiRequest, NextApiResponse } from 'next'
3
4
5
6
7
8
return res.status(200).json({ message: 'Hello World' })
9
10
return res.status(200).json({ message: 'Hello World' })
// pages/api/hello.ts -> <host>/api/hello
1
2
import type { NextApiRequest, NextApiResponse } from 'next'
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<{ message: string }>
7
) {
8
9
}
10
// pages/api/hello.ts -> <host>/api/hello
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler (
req: NextApiRequest,
res: NextApiResponse<{ message: string }>
) {
return res.status(200).json({ message: 'Hello World' })
}
1
2
3
4
5
6
7
8
9
10
APIS WITH NEXT.JS
Files inside pages/api are API endpoints
loige
26
// pages/api/invite.ts
import { InviteResponse } from '../../types/invite'
import { getInvite } from '../../utils/airtable'
export default async function handler (
req: NextApiRequest,
res: NextApiResponse<InviteResponse | { error: string }>
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method Not Allowed' })
}
if (!req.query.code) {
return res.status(400).json({ error: 'Missing invite code' })
}
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
try {
const invite = await getInvite(code)
res.status(200).json({ invite })
} catch (err) {
if ((err as Error).message === 'Invite not found') {
return res.status(401).json({ error: 'Invite not found' })
}
res.status(500).json({ error: 'Internal server error' })
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// pages/api/invite.ts
1
import { InviteResponse } from '../../types/invite'
2
import { getInvite } from '../../utils/airtable'
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<InviteResponse | { error: string }>
7
) {
8
if (req.method !== 'GET') {
9
return res.status(405).json({ error: 'Method Not Allowed' })
10
}
11
if (!req.query.code) {
12
return res.status(400).json({ error: 'Missing invite code' })
13
}
14
15
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
16
17
try {
18
const invite = await getInvite(code)
19
res.status(200).json({ invite })
20
} catch (err) {
21
if ((err as Error).message === 'Invite not found') {
22
return res.status(401).json({ error: 'Invite not found' })
23
}
24
res.status(500).json({ error: 'Internal server error' })
25
}
26
}
27
import { InviteResponse } from '../../types/invite'
import { getInvite } from '../../utils/airtable'
// pages/api/invite.ts
1
2
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<InviteResponse | { error: string }>
7
) {
8
if (req.method !== 'GET') {
9
return res.status(405).json({ error: 'Method Not Allowed' })
10
}
11
if (!req.query.code) {
12
return res.status(400).json({ error: 'Missing invite code' })
13
}
14
15
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
16
17
try {
18
const invite = await getInvite(code)
19
res.status(200).json({ invite })
20
} catch (err) {
21
if ((err as Error).message === 'Invite not found') {
22
return res.status(401).json({ error: 'Invite not found' })
23
}
24
res.status(500).json({ error: 'Internal server error' })
25
}
26
}
27
export default async function handler (
req: NextApiRequest,
res: NextApiResponse<InviteResponse | { error: string }>
) {
}
// pages/api/invite.ts
1
import { InviteResponse } from '../../types/invite'
2
import { getInvite } from '../../utils/airtable'
3
4
5
6
7
8
if (req.method !== 'GET') {
9
return res.status(405).json({ error: 'Method Not Allowed' })
10
}
11
if (!req.query.code) {
12
return res.status(400).json({ error: 'Missing invite code' })
13
}
14
15
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
16
17
try {
18
const invite = await getInvite(code)
19
res.status(200).json({ invite })
20
} catch (err) {
21
if ((err as Error).message === 'Invite not found') {
22
return res.status(401).json({ error: 'Invite not found' })
23
}
24
res.status(500).json({ error: 'Internal server error' })
25
}
26
27
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method Not Allowed' })
}
if (!req.query.code) {
return res.status(400).json({ error: 'Missing invite code' })
}
// pages/api/invite.ts
1
import { InviteResponse } from '../../types/invite'
2
import { getInvite } from '../../utils/airtable'
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<InviteResponse | { error: string }>
7
) {
8
9
10
11
12
13
14
15
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
16
17
try {
18
const invite = await getInvite(code)
19
res.status(200).json({ invite })
20
} catch (err) {
21
if ((err as Error).message === 'Invite not found') {
22
return res.status(401).json({ error: 'Invite not found' })
23
}
24
res.status(500).json({ error: 'Internal server error' })
25
}
26
}
27
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
// pages/api/invite.ts
1
import { InviteResponse } from '../../types/invite'
2
import { getInvite } from '../../utils/airtable'
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<InviteResponse | { error: string }>
7
) {
8
if (req.method !== 'GET') {
9
return res.status(405).json({ error: 'Method Not Allowed' })
10
}
11
if (!req.query.code) {
12
return res.status(400).json({ error: 'Missing invite code' })
13
}
14
15
16
17
try {
18
const invite = await getInvite(code)
19
res.status(200).json({ invite })
20
} catch (err) {
21
if ((err as Error).message === 'Invite not found') {
22
return res.status(401).json({ error: 'Invite not found' })
23
}
24
res.status(500).json({ error: 'Internal server error' })
25
}
26
}
27
try {
}
// pages/api/invite.ts
1
import { InviteResponse } from '../../types/invite'
2
import { getInvite } from '../../utils/airtable'
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<InviteResponse | { error: string }>
7
) {
8
if (req.method !== 'GET') {
9
return res.status(405).json({ error: 'Method Not Allowed' })
10
}
11
if (!req.query.code) {
12
return res.status(400).json({ error: 'Missing invite code' })
13
}
14
15
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
16
17
18
const invite = await getInvite(code)
19
res.status(200).json({ invite })
20
} catch (err) {
21
if ((err as Error).message === 'Invite not found') {
22
return res.status(401).json({ error: 'Invite not found' })
23
}
24
res.status(500).json({ error: 'Internal server error' })
25
26
}
27
const invite = await getInvite(code)
res.status(200).json({ invite })
// pages/api/invite.ts
1
import { InviteResponse } from '../../types/invite'
2
import { getInvite } from '../../utils/airtable'
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<InviteResponse | { error: string }>
7
) {
8
if (req.method !== 'GET') {
9
return res.status(405).json({ error: 'Method Not Allowed' })
10
}
11
if (!req.query.code) {
12
return res.status(400).json({ error: 'Missing invite code' })
13
}
14
15
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
16
17
try {
18
19
20
} catch (err) {
21
if ((err as Error).message === 'Invite not found') {
22
return res.status(401).json({ error: 'Invite not found' })
23
}
24
res.status(500).json({ error: 'Internal server error' })
25
}
26
}
27
if ((err as Error).message === 'Invite not found') {
return res.status(401).json({ error: 'Invite not found' })
}
res.status(500).json({ error: 'Internal server error' })
// pages/api/invite.ts
1
import { InviteResponse } from '../../types/invite'
2
import { getInvite } from '../../utils/airtable'
3
4
export default async function handler (
5
req: NextApiRequest,
6
res: NextApiResponse<InviteResponse | { error: string }>
7
) {
8
if (req.method !== 'GET') {
9
return res.status(405).json({ error: 'Method Not Allowed' })
10
}
11
if (!req.query.code) {
12
return res.status(400).json({ error: 'Missing invite code' })
13
}
14
15
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
16
17
try {
18
const invite = await getInvite(code)
19
res.status(200).json({ invite })
20
} catch (err) {
21
22
23
24
25
}
26
}
27
// pages/api/invite.ts
import { InviteResponse } from '../../types/invite'
import { getInvite } from '../../utils/airtable'
export default async function handler (
req: NextApiRequest,
res: NextApiResponse<InviteResponse | { error: string }>
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method Not Allowed' })
}
if (!req.query.code) {
return res.status(400).json({ error: 'Missing invite code' })
}
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
try {
const invite = await getInvite(code)
res.status(200).json({ invite })
} catch (err) {
if ((err as Error).message === 'Invite not found') {
return res.status(401).json({ error: 'Invite not found' })
}
res.status(500).json({ error: 'Internal server error' })
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 loige
27
{
"invite":{
"code":"14b25700-fe5b-45e8-a9be-4863b6239fcf",
"name":"Leonardo",
"favouriteColor":"blue",
"weapon":"Twin Katana"
}
}
loige
curl -XGET "http://localhost:3000/api/invite?code=14b25700-fe5b-45e8-a9be-4863b6239fcf"
TESTING
28
STEP 4.
INVITE VALIDATION IN REACT
loige 29
ATTACK PLAN 🤺
When the SPA loads:
We grab the invite code from the URL
We call the invite API with the code
✅If it's valid, we render the content
❌If it's invalid, we render an error
loige 30
HOW DO WE MANAGE THIS DATA FETCHING LIFECYCLE? 😰
In-line in the top-level component (App)?
In a Context provider?
In a specialized React Hook?
loige 31
HOW CAN WE CREATE A CUSTOM REACT HOOK? 🤓
A custom Hook is a JavaScript function whose name starts with
”use” and that may call other Hooks
It doesn’t need to have a specific signature
Inside the function, all the common rules of hooks apply:
Only call Hooks at the top level
Don’t call Hooks inside loops, conditions, or nested functions
reactjs.org/docs/hooks-custom.html
loige 32
// components/hooks/useInvite.tsx
import { useState, useEffect } from 'react'
import { InviteResponse } from '../../types/invite'
async function fetchInvite (code: string): Promise<InviteResponse> {
// makes a fetch request to the invite api (elided for brevity)
}
export default function useInvite (): [InviteResponse | null, string | null] {
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const url = new URL(window.location.toString())
const code = url.searchParams.get('code')
if (!code) {
setError('No code provided')
} else {
fetchInvite(code)
.then(setInviteResponse)
.catch(err => {
setError(err.message)
})
}
}, [])
return [inviteResponse, error]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
useEffect(() => {
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
if (!code) {
16
setError('No code provided')
17
} else {
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
}
24
}, [])
25
26
return [inviteResponse, error]
27
}
28
import { useState, useEffect } from 'react'
import { InviteResponse } from '../../types/invite'
// components/hooks/useInvite.tsx
1
2
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
useEffect(() => {
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
if (!code) {
16
setError('No code provided')
17
} else {
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
}
24
}, [])
25
26
return [inviteResponse, error]
27
}
28
async function fetchInvite (code: string): Promise<InviteResponse> {
// makes a fetch request to the invite api (elided for brevity)
}
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
4
5
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
useEffect(() => {
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
if (!code) {
16
setError('No code provided')
17
} else {
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
}
24
}, [])
25
26
return [inviteResponse, error]
27
}
28
export default function useInvite (): [InviteResponse | null, string | null] {
}
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
useEffect(() => {
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
if (!code) {
16
setError('No code provided')
17
} else {
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
}
24
}, [])
25
26
return [inviteResponse, error]
27
28
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
const [error, setError] = useState<string | null>(null)
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
9
10
11
useEffect(() => {
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
if (!code) {
16
setError('No code provided')
17
} else {
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
}
24
}, [])
25
26
return [inviteResponse, error]
27
}
28
useEffect(() => {
}, [])
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
if (!code) {
16
setError('No code provided')
17
} else {
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
}
24
25
26
return [inviteResponse, error]
27
}
28
const url = new URL(window.location.toString())
const code = url.searchParams.get('code')
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
useEffect(() => {
12
13
14
15
if (!code) {
16
setError('No code provided')
17
} else {
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
}
24
}, [])
25
26
return [inviteResponse, error]
27
}
28
if (!code) {
} else {
}
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
useEffect(() => {
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
16
setError('No code provided')
17
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
24
}, [])
25
26
return [inviteResponse, error]
27
}
28
setError('No code provided')
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
useEffect(() => {
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
if (!code) {
16
17
} else {
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
}
24
}, [])
25
26
return [inviteResponse, error]
27
}
28
fetchInvite(code)
.then(setInviteResponse)
.catch(err => {
setError(err.message)
})
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
useEffect(() => {
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
if (!code) {
16
setError('No code provided')
17
} else {
18
19
20
21
22
23
}
24
}, [])
25
26
return [inviteResponse, error]
27
}
28
return [inviteResponse, error]
// components/hooks/useInvite.tsx
1
import { useState, useEffect } from 'react'
2
import { InviteResponse } from '../../types/invite'
3
async function fetchInvite (code: string): Promise<InviteResponse> {
4
// makes a fetch request to the invite api (elided for brevity)
5
}
6
7
export default function useInvite (): [InviteResponse | null, string | null] {
8
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
9
const [error, setError] = useState<string | null>(null)
10
11
useEffect(() => {
12
const url = new URL(window.location.toString())
13
const code = url.searchParams.get('code')
14
15
if (!code) {
16
setError('No code provided')
17
} else {
18
fetchInvite(code)
19
.then(setInviteResponse)
20
.catch(err => {
21
setError(err.message)
22
})
23
}
24
}, [])
25
26
27
}
28
// components/hooks/useInvite.tsx
import { useState, useEffect } from 'react'
import { InviteResponse } from '../../types/invite'
async function fetchInvite (code: string): Promise<InviteResponse> {
// makes a fetch request to the invite api (elided for brevity)
}
export default function useInvite (): [InviteResponse | null, string | null] {
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const url = new URL(window.location.toString())
const code = url.searchParams.get('code')
if (!code) {
setError('No code provided')
} else {
fetchInvite(code)
.then(setInviteResponse)
.catch(err => {
setError(err.message)
})
}
}, [])
return [inviteResponse, error]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 loige
33
import React from 'react'
import useInvite from './hooks/useInvite'
export default function SomeExampleComponent () {
const [inviteResponse, error] = useInvite()
// there was an error
if (error) {
return <div>... some error happened</div>
}
// still loading the data from the backend
if (!inviteResponse) {
return <div>Loading ...</div>
}
// has the data!
return <div>
actual component markup when inviteResponse is available
</div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react'
import useInvite from './hooks/useInvite'
1
2
3
export default function SomeExampleComponent () {
4
const [inviteResponse, error] = useInvite()
5
6
// there was an error
7
if (error) {
8
return <div>... some error happened</div>
9
}
10
11
// still loading the data from the backend
12
if (!inviteResponse) {
13
return <div>Loading ...</div>
14
}
15
16
// has the data!
17
return <div>
18
actual component markup when inviteResponse is available
19
</div>
20
}
21
export default function SomeExampleComponent () {
}
import React from 'react'
1
import useInvite from './hooks/useInvite'
2
3
4
const [inviteResponse, error] = useInvite()
5
6
// there was an error
7
if (error) {
8
return <div>... some error happened</div>
9
}
10
11
// still loading the data from the backend
12
if (!inviteResponse) {
13
return <div>Loading ...</div>
14
}
15
16
// has the data!
17
return <div>
18
actual component markup when inviteResponse is available
19
</div>
20
21
const [inviteResponse, error] = useInvite()
import React from 'react'
1
import useInvite from './hooks/useInvite'
2
3
export default function SomeExampleComponent () {
4
5
6
// there was an error
7
if (error) {
8
return <div>... some error happened</div>
9
}
10
11
// still loading the data from the backend
12
if (!inviteResponse) {
13
return <div>Loading ...</div>
14
}
15
16
// has the data!
17
return <div>
18
actual component markup when inviteResponse is available
19
</div>
20
}
21
// there was an error
if (error) {
return <div>... some error happened</div>
}
import React from 'react'
1
import useInvite from './hooks/useInvite'
2
3
export default function SomeExampleComponent () {
4
const [inviteResponse, error] = useInvite()
5
6
7
8
9
10
11
// still loading the data from the backend
12
if (!inviteResponse) {
13
return <div>Loading ...</div>
14
}
15
16
// has the data!
17
return <div>
18
actual component markup when inviteResponse is available
19
</div>
20
}
21
// still loading the data from the backend
if (!inviteResponse) {
return <div>Loading ...</div>
}
import React from 'react'
1
import useInvite from './hooks/useInvite'
2
3
export default function SomeExampleComponent () {
4
const [inviteResponse, error] = useInvite()
5
6
// there was an error
7
if (error) {
8
return <div>... some error happened</div>
9
}
10
11
12
13
14
15
16
// has the data!
17
return <div>
18
actual component markup when inviteResponse is available
19
</div>
20
}
21
// has the data!
return <div>
actual component markup when inviteResponse is available
</div>
import React from 'react'
1
import useInvite from './hooks/useInvite'
2
3
export default function SomeExampleComponent () {
4
const [inviteResponse, error] = useInvite()
5
6
// there was an error
7
if (error) {
8
return <div>... some error happened</div>
9
}
10
11
// still loading the data from the backend
12
if (!inviteResponse) {
13
return <div>Loading ...</div>
14
}
15
16
17
18
19
20
}
21
import React from 'react'
import useInvite from './hooks/useInvite'
export default function SomeExampleComponent () {
const [inviteResponse, error] = useInvite()
// there was an error
if (error) {
return <div>... some error happened</div>
}
// still loading the data from the backend
if (!inviteResponse) {
return <div>Loading ...</div>
}
// has the data!
return <div>
actual component markup when inviteResponse is available
</div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 loige
EXAMPLE USAGE:
34
STEP 5.
COLLECTING USER DATA
loige 35
CHANGES REQUIRED 🙎
Add the "coming" field in Airtable
Add a new backend utility to update the "coming" field for a given
invite
Add a new endpoint to update the coming field for the current user
Update the React hook the expose the update functionality
loige 36
loige
New field
("yes", "no", or undefined)
ADD THE "COMING" FIELD IN AIRTABLE
37
// utils/airtable.ts
import Airtable, { FieldSet, Record } from 'airtable'
// ...
export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> {
// gets the raw record for a given invite, elided for brevity
}
export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> {
const { id } = await getInviteRecord(inviteCode)
return new Promise((resolve, reject) => {
base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => {
if (err) {
return reject(err)
}
resolve()
})
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// utils/airtable.ts
1
import Airtable, { FieldSet, Record } from 'airtable'
2
// ...
3
4
export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> {
5
// gets the raw record for a given invite, elided for brevity
6
}
7
8
export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> {
9
const { id } = await getInviteRecord(inviteCode)
10
11
return new Promise((resolve, reject) => {
12
base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => {
13
if (err) {
14
return reject(err)
15
}
16
17
resolve()
18
})
19
})
20
}
21
import Airtable, { FieldSet, Record } from 'airtable'
export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> {
// gets the raw record for a given invite, elided for brevity
}
// utils/airtable.ts
1
2
// ...
3
4
5
6
7
8
export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> {
9
const { id } = await getInviteRecord(inviteCode)
10
11
return new Promise((resolve, reject) => {
12
base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => {
13
if (err) {
14
return reject(err)
15
}
16
17
resolve()
18
})
19
})
20
}
21
export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> {
}
// utils/airtable.ts
1
import Airtable, { FieldSet, Record } from 'airtable'
2
// ...
3
4
export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> {
5
// gets the raw record for a given invite, elided for brevity
6
}
7
8
9
const { id } = await getInviteRecord(inviteCode)
10
11
return new Promise((resolve, reject) => {
12
base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => {
13
if (err) {
14
return reject(err)
15
}
16
17
resolve()
18
})
19
})
20
21
const { id } = await getInviteRecord(inviteCode)
// utils/airtable.ts
1
import Airtable, { FieldSet, Record } from 'airtable'
2
// ...
3
4
export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> {
5
// gets the raw record for a given invite, elided for brevity
6
}
7
8
export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> {
9
10
11
return new Promise((resolve, reject) => {
12
base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => {
13
if (err) {
14
return reject(err)
15
}
16
17
resolve()
18
})
19
})
20
}
21
return new Promise((resolve, reject) => {
})
// utils/airtable.ts
1
import Airtable, { FieldSet, Record } from 'airtable'
2
// ...
3
4
export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> {
5
// gets the raw record for a given invite, elided for brevity
6
}
7
8
export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> {
9
const { id } = await getInviteRecord(inviteCode)
10
11
12
base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => {
13
if (err) {
14
return reject(err)
15
}
16
17
resolve()
18
})
19
20
}
21
base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => {
})
// utils/airtable.ts
1
import Airtable, { FieldSet, Record } from 'airtable'
2
// ...
3
4
export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> {
5
// gets the raw record for a given invite, elided for brevity
6
}
7
8
export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> {
9
const { id } = await getInviteRecord(inviteCode)
10
11
return new Promise((resolve, reject) => {
12
13
if (err) {
14
return reject(err)
15
}
16
17
resolve()
18
19
})
20
}
21
if (err) {
return reject(err)
}
resolve()
// utils/airtable.ts
1
import Airtable, { FieldSet, Record } from 'airtable'
2
// ...
3
4
export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> {
5
// gets the raw record for a given invite, elided for brevity
6
}
7
8
export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> {
9
const { id } = await getInviteRecord(inviteCode)
10
11
return new Promise((resolve, reject) => {
12
base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => {
13
14
15
16
17
18
})
19
})
20
}
21
// utils/airtable.ts
import Airtable, { FieldSet, Record } from 'airtable'
// ...
export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> {
// gets the raw record for a given invite, elided for brevity
}
export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> {
const { id } = await getInviteRecord(inviteCode)
return new Promise((resolve, reject) => {
base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => {
if (err) {
return reject(err)
}
resolve()
})
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 loige
RSVP UTILITY
38
// pages/api/rsvp.ts
type RequestBody = {coming?: boolean}
export default async function handler (
req: NextApiRequest,
res: NextApiResponse<{updated: boolean} | { error: string }>
) {
if (req.method !== 'PUT') {
return res.status(405).json({ error: 'Method Not Allowed' })
}
if (!req.query.code) {
return res.status(400).json({ error: 'Missing invite code' })
}
const reqBody = req.body as RequestBody
if (typeof reqBody.coming === 'undefined') {
return res.status(400).json({ error: 'Missing `coming` field in body' })
}
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
try {
await updateRsvp(code, reqBody.coming)
return res.status(200).json({ updated: true })
} catch (err) {
if ((err as Error).message === 'Invite not found') {
return res.status(401).json({ error: 'Invite not found' })
}
res.status(500).json({ error: 'Internal server error' })
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// pages/api/rsvp.ts
1
type RequestBody = {coming?: boolean}
2
3
export default async function handler (
4
req: NextApiRequest,
5
res: NextApiResponse<{updated: boolean} | { error: string }>
6
) {
7
if (req.method !== 'PUT') {
8
return res.status(405).json({ error: 'Method Not Allowed' })
9
}
10
if (!req.query.code) {
11
return res.status(400).json({ error: 'Missing invite code' })
12
}
13
const reqBody = req.body as RequestBody
14
if (typeof reqBody.coming === 'undefined') {
15
return res.status(400).json({ error: 'Missing `coming` field in body' })
16
}
17
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
18
19
try {
20
await updateRsvp(code, reqBody.coming)
21
return res.status(200).json({ updated: true })
22
} catch (err) {
23
if ((err as Error).message === 'Invite not found') {
24
return res.status(401).json({ error: 'Invite not found' })
25
}
26
res.status(500).json({ error: 'Internal server error' })
27
}
28
}
29
type RequestBody = {coming?: boolean}
// pages/api/rsvp.ts
1
2
3
export default async function handler (
4
req: NextApiRequest,
5
res: NextApiResponse<{updated: boolean} | { error: string }>
6
) {
7
if (req.method !== 'PUT') {
8
return res.status(405).json({ error: 'Method Not Allowed' })
9
}
10
if (!req.query.code) {
11
return res.status(400).json({ error: 'Missing invite code' })
12
}
13
const reqBody = req.body as RequestBody
14
if (typeof reqBody.coming === 'undefined') {
15
return res.status(400).json({ error: 'Missing `coming` field in body' })
16
}
17
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
18
19
try {
20
await updateRsvp(code, reqBody.coming)
21
return res.status(200).json({ updated: true })
22
} catch (err) {
23
if ((err as Error).message === 'Invite not found') {
24
return res.status(401).json({ error: 'Invite not found' })
25
}
26
res.status(500).json({ error: 'Internal server error' })
27
}
28
}
29
export default async function handler (
req: NextApiRequest,
res: NextApiResponse<{updated: boolean} | { error: string }>
) {
}
// pages/api/rsvp.ts
1
type RequestBody = {coming?: boolean}
2
3
4
5
6
7
if (req.method !== 'PUT') {
8
return res.status(405).json({ error: 'Method Not Allowed' })
9
}
10
if (!req.query.code) {
11
return res.status(400).json({ error: 'Missing invite code' })
12
}
13
const reqBody = req.body as RequestBody
14
if (typeof reqBody.coming === 'undefined') {
15
return res.status(400).json({ error: 'Missing `coming` field in body' })
16
}
17
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
18
19
try {
20
await updateRsvp(code, reqBody.coming)
21
return res.status(200).json({ updated: true })
22
} catch (err) {
23
if ((err as Error).message === 'Invite not found') {
24
return res.status(401).json({ error: 'Invite not found' })
25
}
26
res.status(500).json({ error: 'Internal server error' })
27
}
28
29
if (req.method !== 'PUT') {
return res.status(405).json({ error: 'Method Not Allowed' })
}
if (!req.query.code) {
return res.status(400).json({ error: 'Missing invite code' })
}
// pages/api/rsvp.ts
1
type RequestBody = {coming?: boolean}
2
3
export default async function handler (
4
req: NextApiRequest,
5
res: NextApiResponse<{updated: boolean} | { error: string }>
6
) {
7
8
9
10
11
12
13
const reqBody = req.body as RequestBody
14
if (typeof reqBody.coming === 'undefined') {
15
return res.status(400).json({ error: 'Missing `coming` field in body' })
16
}
17
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
18
19
try {
20
await updateRsvp(code, reqBody.coming)
21
return res.status(200).json({ updated: true })
22
} catch (err) {
23
if ((err as Error).message === 'Invite not found') {
24
return res.status(401).json({ error: 'Invite not found' })
25
}
26
res.status(500).json({ error: 'Internal server error' })
27
}
28
}
29
const reqBody = req.body as RequestBody
if (typeof reqBody.coming === 'undefined') {
return res.status(400).json({ error: 'Missing `coming` field in body' })
}
// pages/api/rsvp.ts
1
type RequestBody = {coming?: boolean}
2
3
export default async function handler (
4
req: NextApiRequest,
5
res: NextApiResponse<{updated: boolean} | { error: string }>
6
) {
7
if (req.method !== 'PUT') {
8
return res.status(405).json({ error: 'Method Not Allowed' })
9
}
10
if (!req.query.code) {
11
return res.status(400).json({ error: 'Missing invite code' })
12
}
13
14
15
16
17
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
18
19
try {
20
await updateRsvp(code, reqBody.coming)
21
return res.status(200).json({ updated: true })
22
} catch (err) {
23
if ((err as Error).message === 'Invite not found') {
24
return res.status(401).json({ error: 'Invite not found' })
25
}
26
res.status(500).json({ error: 'Internal server error' })
27
}
28
}
29
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
// pages/api/rsvp.ts
1
type RequestBody = {coming?: boolean}
2
3
export default async function handler (
4
req: NextApiRequest,
5
res: NextApiResponse<{updated: boolean} | { error: string }>
6
) {
7
if (req.method !== 'PUT') {
8
return res.status(405).json({ error: 'Method Not Allowed' })
9
}
10
if (!req.query.code) {
11
return res.status(400).json({ error: 'Missing invite code' })
12
}
13
const reqBody = req.body as RequestBody
14
if (typeof reqBody.coming === 'undefined') {
15
return res.status(400).json({ error: 'Missing `coming` field in body' })
16
}
17
18
19
try {
20
await updateRsvp(code, reqBody.coming)
21
return res.status(200).json({ updated: true })
22
} catch (err) {
23
if ((err as Error).message === 'Invite not found') {
24
return res.status(401).json({ error: 'Invite not found' })
25
}
26
res.status(500).json({ error: 'Internal server error' })
27
}
28
}
29
try {
} catch (err) {
}
// pages/api/rsvp.ts
1
type RequestBody = {coming?: boolean}
2
3
export default async function handler (
4
req: NextApiRequest,
5
res: NextApiResponse<{updated: boolean} | { error: string }>
6
) {
7
if (req.method !== 'PUT') {
8
return res.status(405).json({ error: 'Method Not Allowed' })
9
}
10
if (!req.query.code) {
11
return res.status(400).json({ error: 'Missing invite code' })
12
}
13
const reqBody = req.body as RequestBody
14
if (typeof reqBody.coming === 'undefined') {
15
return res.status(400).json({ error: 'Missing `coming` field in body' })
16
}
17
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
18
19
20
await updateRsvp(code, reqBody.coming)
21
return res.status(200).json({ updated: true })
22
23
if ((err as Error).message === 'Invite not found') {
24
return res.status(401).json({ error: 'Invite not found' })
25
}
26
res.status(500).json({ error: 'Internal server error' })
27
28
}
29
await updateRsvp(code, reqBody.coming)
return res.status(200).json({ updated: true })
// pages/api/rsvp.ts
1
type RequestBody = {coming?: boolean}
2
3
export default async function handler (
4
req: NextApiRequest,
5
res: NextApiResponse<{updated: boolean} | { error: string }>
6
) {
7
if (req.method !== 'PUT') {
8
return res.status(405).json({ error: 'Method Not Allowed' })
9
}
10
if (!req.query.code) {
11
return res.status(400).json({ error: 'Missing invite code' })
12
}
13
const reqBody = req.body as RequestBody
14
if (typeof reqBody.coming === 'undefined') {
15
return res.status(400).json({ error: 'Missing `coming` field in body' })
16
}
17
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
18
19
try {
20
21
22
} catch (err) {
23
if ((err as Error).message === 'Invite not found') {
24
return res.status(401).json({ error: 'Invite not found' })
25
}
26
res.status(500).json({ error: 'Internal server error' })
27
}
28
}
29
if ((err as Error).message === 'Invite not found') {
return res.status(401).json({ error: 'Invite not found' })
}
res.status(500).json({ error: 'Internal server error' })
// pages/api/rsvp.ts
1
type RequestBody = {coming?: boolean}
2
3
export default async function handler (
4
req: NextApiRequest,
5
res: NextApiResponse<{updated: boolean} | { error: string }>
6
) {
7
if (req.method !== 'PUT') {
8
return res.status(405).json({ error: 'Method Not Allowed' })
9
}
10
if (!req.query.code) {
11
return res.status(400).json({ error: 'Missing invite code' })
12
}
13
const reqBody = req.body as RequestBody
14
if (typeof reqBody.coming === 'undefined') {
15
return res.status(400).json({ error: 'Missing `coming` field in body' })
16
}
17
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
18
19
try {
20
await updateRsvp(code, reqBody.coming)
21
return res.status(200).json({ updated: true })
22
} catch (err) {
23
24
25
26
27
}
28
}
29
// pages/api/rsvp.ts
type RequestBody = {coming?: boolean}
export default async function handler (
req: NextApiRequest,
res: NextApiResponse<{updated: boolean} | { error: string }>
) {
if (req.method !== 'PUT') {
return res.status(405).json({ error: 'Method Not Allowed' })
}
if (!req.query.code) {
return res.status(400).json({ error: 'Missing invite code' })
}
const reqBody = req.body as RequestBody
if (typeof reqBody.coming === 'undefined') {
return res.status(400).json({ error: 'Missing `coming` field in body' })
}
const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code
try {
await updateRsvp(code, reqBody.coming)
return res.status(200).json({ updated: true })
} catch (err) {
if ((err as Error).message === 'Invite not found') {
return res.status(401).json({ error: 'Invite not found' })
}
res.status(500).json({ error: 'Internal server error' })
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
loige
RSVP API ENDPOINT
39
// components/hooks/useInvite.tsx
// ...
interface HookResult {
inviteResponse: InviteResponse | null,
error: string | null,
updating: boolean,
updateRsvp: (coming: boolean) => Promise<void>
}
async function updateRsvpRequest (code: string, coming: boolean): Promise<void> {
// Helper function that uses fetch to invoke the rsvp API endpoint (elided)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// components/hooks/useInvite.tsx
// ...
1
2
3
interface HookResult {
4
inviteResponse: InviteResponse | null,
5
error: string | null,
6
updating: boolean,
7
updateRsvp: (coming: boolean) => Promise<void>
8
}
9
10
async function updateRsvpRequest (code: string, coming: boolean): Promise<void> {
11
// Helper function that uses fetch to invoke the rsvp API endpoint (elided)
12
}
13
interface HookResult {
inviteResponse: InviteResponse | null,
error: string | null,
updating: boolean,
updateRsvp: (coming: boolean) => Promise<void>
}
// components/hooks/useInvite.tsx
1
// ...
2
3
4
5
6
7
8
9
10
async function updateRsvpRequest (code: string, coming: boolean): Promise<void> {
11
// Helper function that uses fetch to invoke the rsvp API endpoint (elided)
12
}
13
// components/hooks/useInvite.tsx
// ...
interface HookResult {
inviteResponse: InviteResponse | null,
error: string | null,
updating: boolean,
updateRsvp: (coming: boolean) => Promise<void>
}
async function updateRsvpRequest (code: string, coming: boolean): Promise<void> {
// Helper function that uses fetch to invoke the rsvp API endpoint (elided)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// components/hooks/useInvite.tsx
// ...
interface HookResult {
inviteResponse: InviteResponse | null,
error: string | null,
updating: boolean,
updateRsvp: (coming: boolean) => Promise<void>
}
async function updateRsvpRequest (code: string, coming: boolean): Promise<void> {
// Helper function that uses fetch to invoke the rsvp API endpoint (elided)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
loige
USEINVITE HOOK V2
40
// ...
export default function useInvite (): HookResult {
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
const [error, setError] = useState<string | null>(null)
const [updating, setUpdating] = useState<boolean>(false)
useEffect(() => {
// load the invite using the code from URL, same as before
}, [])
async function updateRsvp (coming: boolean) {
if (inviteResponse) {
setUpdating(true)
await updateRsvpRequest(inviteResponse.invite.code, coming)
setInviteResponse({
...inviteResponse,
invite: { ...inviteResponse.invite, coming }
})
setUpdating(false)
}
}
return { inviteResponse, error, updating, updateRsvp }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default function useInvite (): HookResult {
}
// ...
1
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
async function updateRsvp (coming: boolean) {
11
if (inviteResponse) {
12
setUpdating(true)
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
setUpdating(false)
19
}
20
}
21
22
return { inviteResponse, error, updating, updateRsvp }
23
24
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
const [error, setError] = useState<string | null>(null)
// ...
1
export default function useInvite (): HookResult {
2
3
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
async function updateRsvp (coming: boolean) {
11
if (inviteResponse) {
12
setUpdating(true)
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
setUpdating(false)
19
}
20
}
21
22
return { inviteResponse, error, updating, updateRsvp }
23
}
24
const [updating, setUpdating] = useState<boolean>(false)
// ...
1
export default function useInvite (): HookResult {
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
async function updateRsvp (coming: boolean) {
11
if (inviteResponse) {
12
setUpdating(true)
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
setUpdating(false)
19
}
20
}
21
22
return { inviteResponse, error, updating, updateRsvp }
23
}
24
useEffect(() => {
// load the invite using the code from URL, same as before
}, [])
// ...
1
export default function useInvite (): HookResult {
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
7
8
9
10
async function updateRsvp (coming: boolean) {
11
if (inviteResponse) {
12
setUpdating(true)
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
setUpdating(false)
19
}
20
}
21
22
return { inviteResponse, error, updating, updateRsvp }
23
}
24
async function updateRsvp (coming: boolean) {
}
// ...
1
export default function useInvite (): HookResult {
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
11
if (inviteResponse) {
12
setUpdating(true)
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
setUpdating(false)
19
}
20
21
22
return { inviteResponse, error, updating, updateRsvp }
23
}
24
if (inviteResponse) {
}
// ...
1
export default function useInvite (): HookResult {
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
async function updateRsvp (coming: boolean) {
11
12
setUpdating(true)
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
setUpdating(false)
19
20
}
21
22
return { inviteResponse, error, updating, updateRsvp }
23
}
24
setUpdating(true)
// ...
1
export default function useInvite (): HookResult {
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
async function updateRsvp (coming: boolean) {
11
if (inviteResponse) {
12
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
setUpdating(false)
19
}
20
}
21
22
return { inviteResponse, error, updating, updateRsvp }
23
}
24
await updateRsvpRequest(inviteResponse.invite.code, coming)
// ...
1
export default function useInvite (): HookResult {
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
async function updateRsvp (coming: boolean) {
11
if (inviteResponse) {
12
setUpdating(true)
13
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
setUpdating(false)
19
}
20
}
21
22
return { inviteResponse, error, updating, updateRsvp }
23
}
24
setInviteResponse({
...inviteResponse,
invite: { ...inviteResponse.invite, coming }
})
// ...
1
export default function useInvite (): HookResult {
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
async function updateRsvp (coming: boolean) {
11
if (inviteResponse) {
12
setUpdating(true)
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
15
16
17
18
setUpdating(false)
19
}
20
}
21
22
return { inviteResponse, error, updating, updateRsvp }
23
}
24
setUpdating(false)
// ...
1
export default function useInvite (): HookResult {
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
async function updateRsvp (coming: boolean) {
11
if (inviteResponse) {
12
setUpdating(true)
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
19
}
20
}
21
22
return { inviteResponse, error, updating, updateRsvp }
23
}
24
return { inviteResponse, error, updating, updateRsvp }
// ...
1
export default function useInvite (): HookResult {
2
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
3
const [error, setError] = useState<string | null>(null)
4
const [updating, setUpdating] = useState<boolean>(false)
5
6
useEffect(() => {
7
// load the invite using the code from URL, same as before
8
}, [])
9
10
async function updateRsvp (coming: boolean) {
11
if (inviteResponse) {
12
setUpdating(true)
13
await updateRsvpRequest(inviteResponse.invite.code, coming)
14
setInviteResponse({
15
...inviteResponse,
16
invite: { ...inviteResponse.invite, coming }
17
})
18
setUpdating(false)
19
}
20
}
21
22
23
}
24
// ...
export default function useInvite (): HookResult {
const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null)
const [error, setError] = useState<string | null>(null)
const [updating, setUpdating] = useState<boolean>(false)
useEffect(() => {
// load the invite using the code from URL, same as before
}, [])
async function updateRsvp (coming: boolean) {
if (inviteResponse) {
setUpdating(true)
await updateRsvpRequest(inviteResponse.invite.code, coming)
setInviteResponse({
...inviteResponse,
invite: { ...inviteResponse.invite, coming }
})
setUpdating(false)
}
}
return { inviteResponse, error, updating, updateRsvp }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 loige
41
import useInvite from './hooks/useInvite'
export default function Home () {
const { inviteResponse, error, updating, updateRsvp } = useInvite()
if (error) { return <div>Duh! {error}</div> }
if (!inviteResponse) { return <div>Loading...</div> }
function onRsvpChange (e: ChangeEvent<HTMLInputElement>) {
const coming = e.target.value === 'yes'
updateRsvp(coming)
}
return (<fieldset disabled={updating}><legend>Are you coming?</legend>
<label htmlFor="yes">
<input type="radio" id="yes" name="coming" value="yes"
onChange={onRsvpChange}
checked={inviteResponse.invite.coming === true}
/> YES
</label>
<label htmlFor="no">
<input type="radio" id="no" name="coming" value="no"
onChange={onRsvpChange}
checked={inviteResponse.invite.coming === false}
/> NO
</label>
</fieldset>)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import useInvite from './hooks/useInvite'
1
2
export default function Home () {
3
const { inviteResponse, error, updating, updateRsvp } = useInvite()
4
5
if (error) { return <div>Duh! {error}</div> }
6
if (!inviteResponse) { return <div>Loading...</div> }
7
8
function onRsvpChange (e: ChangeEvent<HTMLInputElement>) {
9
const coming = e.target.value === 'yes'
10
updateRsvp(coming)
11
}
12
13
return (<fieldset disabled={updating}><legend>Are you coming?</legend>
14
<label htmlFor="yes">
15
<input type="radio" id="yes" name="coming" value="yes"
16
onChange={onRsvpChange}
17
checked={inviteResponse.invite.coming === true}
18
/> YES
19
</label>
20
<label htmlFor="no">
21
<input type="radio" id="no" name="coming" value="no"
22
onChange={onRsvpChange}
23
checked={inviteResponse.invite.coming === false}
24
/> NO
25
</label>
26
</fieldset>)
27
}
28
export default function Home () {
}
import useInvite from './hooks/useInvite'
1
2
3
const { inviteResponse, error, updating, updateRsvp } = useInvite()
4
5
if (error) { return <div>Duh! {error}</div> }
6
if (!inviteResponse) { return <div>Loading...</div> }
7
8
function onRsvpChange (e: ChangeEvent<HTMLInputElement>) {
9
const coming = e.target.value === 'yes'
10
updateRsvp(coming)
11
}
12
13
return (<fieldset disabled={updating}><legend>Are you coming?</legend>
14
<label htmlFor="yes">
15
<input type="radio" id="yes" name="coming" value="yes"
16
onChange={onRsvpChange}
17
checked={inviteResponse.invite.coming === true}
18
/> YES
19
</label>
20
<label htmlFor="no">
21
<input type="radio" id="no" name="coming" value="no"
22
onChange={onRsvpChange}
23
checked={inviteResponse.invite.coming === false}
24
/> NO
25
</label>
26
</fieldset>)
27
28
const { inviteResponse, error, updating, updateRsvp } = useInvite()
import useInvite from './hooks/useInvite'
1
2
export default function Home () {
3
4
5
if (error) { return <div>Duh! {error}</div> }
6
if (!inviteResponse) { return <div>Loading...</div> }
7
8
function onRsvpChange (e: ChangeEvent<HTMLInputElement>) {
9
const coming = e.target.value === 'yes'
10
updateRsvp(coming)
11
}
12
13
return (<fieldset disabled={updating}><legend>Are you coming?</legend>
14
<label htmlFor="yes">
15
<input type="radio" id="yes" name="coming" value="yes"
16
onChange={onRsvpChange}
17
checked={inviteResponse.invite.coming === true}
18
/> YES
19
</label>
20
<label htmlFor="no">
21
<input type="radio" id="no" name="coming" value="no"
22
onChange={onRsvpChange}
23
checked={inviteResponse.invite.coming === false}
24
/> NO
25
</label>
26
</fieldset>)
27
}
28
if (error) { return <div>Duh! {error}</div> }
if (!inviteResponse) { return <div>Loading...</div> }
import useInvite from './hooks/useInvite'
1
2
export default function Home () {
3
const { inviteResponse, error, updating, updateRsvp } = useInvite()
4
5
6
7
8
function onRsvpChange (e: ChangeEvent<HTMLInputElement>) {
9
const coming = e.target.value === 'yes'
10
updateRsvp(coming)
11
}
12
13
return (<fieldset disabled={updating}><legend>Are you coming?</legend>
14
<label htmlFor="yes">
15
<input type="radio" id="yes" name="coming" value="yes"
16
onChange={onRsvpChange}
17
checked={inviteResponse.invite.coming === true}
18
/> YES
19
</label>
20
<label htmlFor="no">
21
<input type="radio" id="no" name="coming" value="no"
22
onChange={onRsvpChange}
23
checked={inviteResponse.invite.coming === false}
24
/> NO
25
</label>
26
</fieldset>)
27
}
28
function onRsvpChange (e: ChangeEvent<HTMLInputElement>) {
const coming = e.target.value === 'yes'
updateRsvp(coming)
}
import useInvite from './hooks/useInvite'
1
2
export default function Home () {
3
const { inviteResponse, error, updating, updateRsvp } = useInvite()
4
5
if (error) { return <div>Duh! {error}</div> }
6
if (!inviteResponse) { return <div>Loading...</div> }
7
8
9
10
11
12
13
return (<fieldset disabled={updating}><legend>Are you coming?</legend>
14
<label htmlFor="yes">
15
<input type="radio" id="yes" name="coming" value="yes"
16
onChange={onRsvpChange}
17
checked={inviteResponse.invite.coming === true}
18
/> YES
19
</label>
20
<label htmlFor="no">
21
<input type="radio" id="no" name="coming" value="no"
22
onChange={onRsvpChange}
23
checked={inviteResponse.invite.coming === false}
24
/> NO
25
</label>
26
</fieldset>)
27
}
28
return (<fieldset disabled={updating}><legend>Are you coming?</legend>
<label htmlFor="yes">
<input type="radio" id="yes" name="coming" value="yes"
onChange={onRsvpChange}
checked={inviteResponse.invite.coming === true}
/> YES
</label>
<label htmlFor="no">
<input type="radio" id="no" name="coming" value="no"
onChange={onRsvpChange}
checked={inviteResponse.invite.coming === false}
/> NO
</label>
</fieldset>)
import useInvite from './hooks/useInvite'
1
2
export default function Home () {
3
const { inviteResponse, error, updating, updateRsvp } = useInvite()
4
5
if (error) { return <div>Duh! {error}</div> }
6
if (!inviteResponse) { return <div>Loading...</div> }
7
8
function onRsvpChange (e: ChangeEvent<HTMLInputElement>) {
9
const coming = e.target.value === 'yes'
10
updateRsvp(coming)
11
}
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
}
28
onChange={onRsvpChange}
checked={inviteResponse.invite.coming === true}
onChange={onRsvpChange}
checked={inviteResponse.invite.coming === false}
import useInvite from './hooks/useInvite'
1
2
export default function Home () {
3
const { inviteResponse, error, updating, updateRsvp } = useInvite()
4
5
if (error) { return <div>Duh! {error}</div> }
6
if (!inviteResponse) { return <div>Loading...</div> }
7
8
function onRsvpChange (e: ChangeEvent<HTMLInputElement>) {
9
const coming = e.target.value === 'yes'
10
updateRsvp(coming)
11
}
12
13
return (<fieldset disabled={updating}><legend>Are you coming?</legend>
14
<label htmlFor="yes">
15
<input type="radio" id="yes" name="coming" value="yes"
16
17
18
/> YES
19
</label>
20
<label htmlFor="no">
21
<input type="radio" id="no" name="coming" value="no"
22
23
24
/> NO
25
</label>
26
</fieldset>)
27
}
28
import useInvite from './hooks/useInvite'
export default function Home () {
const { inviteResponse, error, updating, updateRsvp } = useInvite()
if (error) { return <div>Duh! {error}</div> }
if (!inviteResponse) { return <div>Loading...</div> }
function onRsvpChange (e: ChangeEvent<HTMLInputElement>) {
const coming = e.target.value === 'yes'
updateRsvp(coming)
}
return (<fieldset disabled={updating}><legend>Are you coming?</legend>
<label htmlFor="yes">
<input type="radio" id="yes" name="coming" value="yes"
onChange={onRsvpChange}
checked={inviteResponse.invite.coming === true}
/> YES
</label>
<label htmlFor="no">
<input type="radio" id="no" name="coming" value="no"
onChange={onRsvpChange}
checked={inviteResponse.invite.coming === false}
/> NO
</label>
</fieldset>)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 loige
USING THE HOOK
42
15 SECONDS DEMO ⏱
loige 43
loige 44
DEPLOYMENT 🚢
loige 45
loige 46
SECURITY CONSIDERATIONS
loige
What if I don't have an invite code and I want to hack into the website anyway?
47
DISCLOSING SENSITIVE INFORMATION IN THE SOURCE CODE
loige 48
HOW DO WE FIX THIS?
Don't hardcode any sensitive info in your JSX (or JS in general)
Use the invite API to return any sensitive info (together with the
user data)
This way, the sensitive data is available only in the backend code
loige 49
AIRTABLE FILTER FORMULA INJECTION
loige
export function getInvite (inviteCode: string): Promise<Invite> {
return new Promise((resolve, reject) => {
base('invites')
.select({
filterByFormula: `{invite} = ${escape(inviteCode)}`,
maxRecords: 1
})
.firstPage((err, records) => {
// ...
})
})
}
1
2
3
4
5
6
7
8
9
10
11
12
filterByFormula: `{invite} = ${escape(inviteCode)}`,
export function getInvite (inviteCode: string): Promise<Invite> {
1
return new Promise((resolve, reject) => {
2
base('invites')
3
.select({
4
5
maxRecords: 1
6
})
7
.firstPage((err, records) => {
8
// ...
9
})
10
})
11
}
12
50
AIRTABLE FILTER FORMULA INJECTION
loige
filterByFormula: `{invite} = '${inviteCode}'`,
export function getInvite (inviteCode: string): Promise<Invite> {
1
return new Promise((resolve, reject) => {
2
base('invites')
3
.select({
4
5
maxRecords: 1
6
})
7
.firstPage((err, records) => {
8
// ...
9
})
10
})
11
}
12
inviteCode is user controlled!
The user can change this value arbitrarily! 😈 51
SO WHAT?!
If the user inputs the following query string:
loige
?code=14b25700-fe5b-45e8-a9be-4863b6239fcf
We get the following filter formula
{invite} = '14b25700-fe5b-45e8-a9be-4863b6239fcf'
👌52
SO WHAT?!
But, if the user inputs this other query string: 😈
loige
?code=%27%20>%3D%200%20%26%20%27
Which is basically the following unencoded query string:
{invite} = '' >= 0 & ''
😰
?code=' >= 0 & '
Now we get:
which is TRUE for EVERY RECORD!
53
loige 54
HOW DO WE FIX THIS?
The escape function "sanitizes"
user input to try to prevent
injection
Unfortunately Airtable does not
provide an official solution for this,
so this escape function is the best I
could come up with, but it might not
be "good enough"! 😔
loige
function escape (value: string): string {
if (value === null ||
typeof value === 'undefined') {
return 'BLANK()'
}
if (typeof value === 'string') {
const escapedString = value
.replace(/'/g, "'")
.replace(/r/g, '')
.replace(//g, '')
.replace(/n/g, 'n')
.replace(/t/g, 't')
return `'${escapedString}'`
}
if (typeof value === 'number') {
return String(value)
}
if (typeof value === 'boolean') {
return value ? '1' : '0'
}
throw Error('Invalid value received')
}
55
LET'S WRAP THINGS UP... 🌯
loige 56
LIMITATIONS
Airtable API rate limiting: 5 req/sec 😰
(We actually do 2 calls when we update a record!)
loige 57
POSSIBLE ALTERNATIVES
Google spreadsheet (there's an and a )
DynamoDB (with Amplify)
Firebase (?)
Any headless CMS (?)
Supabase or Strapi (?)
API package
loige 58
TAKEAWAYS
This solution is a quick, easy, and cheap way to build invite-only
websites.
We learned about Next.js API endpoints, custom React Hooks,
how to use AirTable (and its SDK), and a bunch of security related
things.
Don't use this solution blindly: evaluate your context and find the
best tech stack!
loige 59
ALSO AVAILABLE AS AN ARTICLE
With the full !
codebase on GitHub
loige
loige.link/microsite-article
60
Cover photo by on
Jakob Owens Unsplash
fourtheorem.com
GRAZIE! 🍕❤
loige.link/pizzaparty
loige 61

More Related Content

Similar to Building an invite-only microsite with Next.js & Airtable - ReactJS Milano

Single Page Web Applications with CoffeeScript, Backbone and Jasmine
Single Page Web Applications with CoffeeScript, Backbone and JasmineSingle Page Web Applications with CoffeeScript, Backbone and Jasmine
Single Page Web Applications with CoffeeScript, Backbone and JasminePaulo Ragonha
 
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and JasmineRails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and JasmineRaimonds Simanovskis
 
Finch.io - Purely Functional REST API with Finagle
Finch.io - Purely Functional REST API with FinagleFinch.io - Purely Functional REST API with Finagle
Finch.io - Purely Functional REST API with FinagleVladimir Kostyukov
 
DevOpsCon 2021: Go Web Development 101
DevOpsCon 2021: Go Web Development 101DevOpsCon 2021: Go Web Development 101
DevOpsCon 2021: Go Web Development 101Jan Stamer
 
Monitoring Your ISP Using InfluxDB Cloud and Raspberry Pi
Monitoring Your ISP Using InfluxDB Cloud and Raspberry PiMonitoring Your ISP Using InfluxDB Cloud and Raspberry Pi
Monitoring Your ISP Using InfluxDB Cloud and Raspberry PiInfluxData
 
entwickler.de Go Day: Go Web Development 101
entwickler.de Go Day: Go Web Development 101entwickler.de Go Day: Go Web Development 101
entwickler.de Go Day: Go Web Development 101Jan Stamer
 
Refresh Austin - Intro to Dexy
Refresh Austin - Intro to DexyRefresh Austin - Intro to Dexy
Refresh Austin - Intro to Dexyananelson
 
How to send gzipped requests with boto3
How to send gzipped requests with boto3How to send gzipped requests with boto3
How to send gzipped requests with boto3Luciano Mammino
 
Quick and Easy Development with Node.js and Couchbase Server
Quick and Easy Development with Node.js and Couchbase ServerQuick and Easy Development with Node.js and Couchbase Server
Quick and Easy Development with Node.js and Couchbase ServerNic Raboy
 
FrenchKit 2017: Server(less) Swift
FrenchKit 2017: Server(less) SwiftFrenchKit 2017: Server(less) Swift
FrenchKit 2017: Server(less) SwiftChris Bailey
 
Ch ch-changes cake php2
Ch ch-changes cake php2Ch ch-changes cake php2
Ch ch-changes cake php2markstory
 
More than syntax
More than syntaxMore than syntax
More than syntaxWooga
 

Similar to Building an invite-only microsite with Next.js & Airtable - ReactJS Milano (20)

Complex Sites with Silex
Complex Sites with SilexComplex Sites with Silex
Complex Sites with Silex
 
Single Page Web Applications with CoffeeScript, Backbone and Jasmine
Single Page Web Applications with CoffeeScript, Backbone and JasmineSingle Page Web Applications with CoffeeScript, Backbone and Jasmine
Single Page Web Applications with CoffeeScript, Backbone and Jasmine
 
Introduction to Groovy
Introduction to GroovyIntroduction to Groovy
Introduction to Groovy
 
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and JasmineRails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
 
NodeJS "Web en tiempo real"
NodeJS "Web en tiempo real"NodeJS "Web en tiempo real"
NodeJS "Web en tiempo real"
 
Finch.io - Purely Functional REST API with Finagle
Finch.io - Purely Functional REST API with FinagleFinch.io - Purely Functional REST API with Finagle
Finch.io - Purely Functional REST API with Finagle
 
dotCloud and go
dotCloud and godotCloud and go
dotCloud and go
 
DevOpsCon 2021: Go Web Development 101
DevOpsCon 2021: Go Web Development 101DevOpsCon 2021: Go Web Development 101
DevOpsCon 2021: Go Web Development 101
 
Monitoring Your ISP Using InfluxDB Cloud and Raspberry Pi
Monitoring Your ISP Using InfluxDB Cloud and Raspberry PiMonitoring Your ISP Using InfluxDB Cloud and Raspberry Pi
Monitoring Your ISP Using InfluxDB Cloud and Raspberry Pi
 
entwickler.de Go Day: Go Web Development 101
entwickler.de Go Day: Go Web Development 101entwickler.de Go Day: Go Web Development 101
entwickler.de Go Day: Go Web Development 101
 
W-JAX 09 - ScalaModules
W-JAX 09 - ScalaModulesW-JAX 09 - ScalaModules
W-JAX 09 - ScalaModules
 
Refresh Austin - Intro to Dexy
Refresh Austin - Intro to DexyRefresh Austin - Intro to Dexy
Refresh Austin - Intro to Dexy
 
Webauthn Tutorial
Webauthn TutorialWebauthn Tutorial
Webauthn Tutorial
 
How to send gzipped requests with boto3
How to send gzipped requests with boto3How to send gzipped requests with boto3
How to send gzipped requests with boto3
 
Quick and Easy Development with Node.js and Couchbase Server
Quick and Easy Development with Node.js and Couchbase ServerQuick and Easy Development with Node.js and Couchbase Server
Quick and Easy Development with Node.js and Couchbase Server
 
Service Workers
Service WorkersService Workers
Service Workers
 
Advanced redux
Advanced reduxAdvanced redux
Advanced redux
 
FrenchKit 2017: Server(less) Swift
FrenchKit 2017: Server(less) SwiftFrenchKit 2017: Server(less) Swift
FrenchKit 2017: Server(less) Swift
 
Ch ch-changes cake php2
Ch ch-changes cake php2Ch ch-changes cake php2
Ch ch-changes cake php2
 
More than syntax
More than syntaxMore than syntax
More than syntax
 

More from Luciano Mammino

Did you know JavaScript has iterators? DublinJS
Did you know JavaScript has iterators? DublinJSDid you know JavaScript has iterators? DublinJS
Did you know JavaScript has iterators? DublinJSLuciano Mammino
 
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...Luciano Mammino
 
From Node.js to Design Patterns - BuildPiper
From Node.js to Design Patterns - BuildPiperFrom Node.js to Design Patterns - BuildPiper
From Node.js to Design Patterns - BuildPiperLuciano Mammino
 
Everything I know about S3 pre-signed URLs
Everything I know about S3 pre-signed URLsEverything I know about S3 pre-signed URLs
Everything I know about S3 pre-signed URLsLuciano Mammino
 
Serverless for High Performance Computing
Serverless for High Performance ComputingServerless for High Performance Computing
Serverless for High Performance ComputingLuciano Mammino
 
Serverless for High Performance Computing
Serverless for High Performance ComputingServerless for High Performance Computing
Serverless for High Performance ComputingLuciano Mammino
 
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022Luciano Mammino
 
Building an invite-only microsite with Next.js & Airtable
Building an invite-only microsite with Next.js & AirtableBuilding an invite-only microsite with Next.js & Airtable
Building an invite-only microsite with Next.js & AirtableLuciano Mammino
 
Let's take the monolith to the cloud 🚀
Let's take the monolith to the cloud 🚀Let's take the monolith to the cloud 🚀
Let's take the monolith to the cloud 🚀Luciano Mammino
 
A look inside the European Covid Green Certificate - Rust Dublin
A look inside the European Covid Green Certificate - Rust DublinA look inside the European Covid Green Certificate - Rust Dublin
A look inside the European Covid Green Certificate - Rust DublinLuciano Mammino
 
Node.js: scalability tips - Azure Dev Community Vijayawada
Node.js: scalability tips - Azure Dev Community VijayawadaNode.js: scalability tips - Azure Dev Community Vijayawada
Node.js: scalability tips - Azure Dev Community VijayawadaLuciano Mammino
 
A look inside the European Covid Green Certificate (Codemotion 2021)
A look inside the European Covid Green Certificate (Codemotion 2021)A look inside the European Covid Green Certificate (Codemotion 2021)
A look inside the European Covid Green Certificate (Codemotion 2021)Luciano Mammino
 
AWS Observability Made Simple
AWS Observability Made SimpleAWS Observability Made Simple
AWS Observability Made SimpleLuciano Mammino
 
Semplificare l'observability per progetti Serverless
Semplificare l'observability per progetti ServerlessSemplificare l'observability per progetti Serverless
Semplificare l'observability per progetti ServerlessLuciano Mammino
 
Finding a lost song with Node.js and async iterators - NodeConf Remote 2021
Finding a lost song with Node.js and async iterators - NodeConf Remote 2021Finding a lost song with Node.js and async iterators - NodeConf Remote 2021
Finding a lost song with Node.js and async iterators - NodeConf Remote 2021Luciano Mammino
 
Finding a lost song with Node.js and async iterators - EnterJS 2021
Finding a lost song with Node.js and async iterators - EnterJS 2021Finding a lost song with Node.js and async iterators - EnterJS 2021
Finding a lost song with Node.js and async iterators - EnterJS 2021Luciano Mammino
 
Finding a lost song with Node.js and async iterators
Finding a lost song with Node.js and async iteratorsFinding a lost song with Node.js and async iterators
Finding a lost song with Node.js and async iteratorsLuciano Mammino
 
AWS Observability (without the Pain)
AWS Observability (without the Pain)AWS Observability (without the Pain)
AWS Observability (without the Pain)Luciano Mammino
 

More from Luciano Mammino (20)

Did you know JavaScript has iterators? DublinJS
Did you know JavaScript has iterators? DublinJSDid you know JavaScript has iterators? DublinJS
Did you know JavaScript has iterators? DublinJS
 
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
 
From Node.js to Design Patterns - BuildPiper
From Node.js to Design Patterns - BuildPiperFrom Node.js to Design Patterns - BuildPiper
From Node.js to Design Patterns - BuildPiper
 
Everything I know about S3 pre-signed URLs
Everything I know about S3 pre-signed URLsEverything I know about S3 pre-signed URLs
Everything I know about S3 pre-signed URLs
 
Serverless for High Performance Computing
Serverless for High Performance ComputingServerless for High Performance Computing
Serverless for High Performance Computing
 
Serverless for High Performance Computing
Serverless for High Performance ComputingServerless for High Performance Computing
Serverless for High Performance Computing
 
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
 
Building an invite-only microsite with Next.js & Airtable
Building an invite-only microsite with Next.js & AirtableBuilding an invite-only microsite with Next.js & Airtable
Building an invite-only microsite with Next.js & Airtable
 
Let's take the monolith to the cloud 🚀
Let's take the monolith to the cloud 🚀Let's take the monolith to the cloud 🚀
Let's take the monolith to the cloud 🚀
 
A look inside the European Covid Green Certificate - Rust Dublin
A look inside the European Covid Green Certificate - Rust DublinA look inside the European Covid Green Certificate - Rust Dublin
A look inside the European Covid Green Certificate - Rust Dublin
 
Monoliths to the cloud!
Monoliths to the cloud!Monoliths to the cloud!
Monoliths to the cloud!
 
The senior dev
The senior devThe senior dev
The senior dev
 
Node.js: scalability tips - Azure Dev Community Vijayawada
Node.js: scalability tips - Azure Dev Community VijayawadaNode.js: scalability tips - Azure Dev Community Vijayawada
Node.js: scalability tips - Azure Dev Community Vijayawada
 
A look inside the European Covid Green Certificate (Codemotion 2021)
A look inside the European Covid Green Certificate (Codemotion 2021)A look inside the European Covid Green Certificate (Codemotion 2021)
A look inside the European Covid Green Certificate (Codemotion 2021)
 
AWS Observability Made Simple
AWS Observability Made SimpleAWS Observability Made Simple
AWS Observability Made Simple
 
Semplificare l'observability per progetti Serverless
Semplificare l'observability per progetti ServerlessSemplificare l'observability per progetti Serverless
Semplificare l'observability per progetti Serverless
 
Finding a lost song with Node.js and async iterators - NodeConf Remote 2021
Finding a lost song with Node.js and async iterators - NodeConf Remote 2021Finding a lost song with Node.js and async iterators - NodeConf Remote 2021
Finding a lost song with Node.js and async iterators - NodeConf Remote 2021
 
Finding a lost song with Node.js and async iterators - EnterJS 2021
Finding a lost song with Node.js and async iterators - EnterJS 2021Finding a lost song with Node.js and async iterators - EnterJS 2021
Finding a lost song with Node.js and async iterators - EnterJS 2021
 
Finding a lost song with Node.js and async iterators
Finding a lost song with Node.js and async iteratorsFinding a lost song with Node.js and async iterators
Finding a lost song with Node.js and async iterators
 
AWS Observability (without the Pain)
AWS Observability (without the Pain)AWS Observability (without the Pain)
AWS Observability (without the Pain)
 

Recently uploaded

CloudStudio User manual (basic edition):
CloudStudio User manual (basic edition):CloudStudio User manual (basic edition):
CloudStudio User manual (basic edition):comworks
 
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptxMaking_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptxnull - The Open Security Community
 
IAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI SolutionsIAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI SolutionsEnterprise Knowledge
 
Enhancing Worker Digital Experience: A Hands-on Workshop for Partners
Enhancing Worker Digital Experience: A Hands-on Workshop for PartnersEnhancing Worker Digital Experience: A Hands-on Workshop for Partners
Enhancing Worker Digital Experience: A Hands-on Workshop for PartnersThousandEyes
 
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking MenDelhi Call girls
 
How to Remove Document Management Hurdles with X-Docs?
How to Remove Document Management Hurdles with X-Docs?How to Remove Document Management Hurdles with X-Docs?
How to Remove Document Management Hurdles with X-Docs?XfilesPro
 
Next-generation AAM aircraft unveiled by Supernal, S-A2
Next-generation AAM aircraft unveiled by Supernal, S-A2Next-generation AAM aircraft unveiled by Supernal, S-A2
Next-generation AAM aircraft unveiled by Supernal, S-A2Hyundai Motor Group
 
Slack Application Development 101 Slides
Slack Application Development 101 SlidesSlack Application Development 101 Slides
Slack Application Development 101 Slidespraypatel2
 
08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking Men08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking MenDelhi Call girls
 
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure serviceWhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure servicePooja Nehwal
 
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...shyamraj55
 
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 3652toLead Limited
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhisoniya singh
 
Artificial intelligence in the post-deep learning era
Artificial intelligence in the post-deep learning eraArtificial intelligence in the post-deep learning era
Artificial intelligence in the post-deep learning eraDeakin University
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationMichael W. Hawkins
 
AI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsAI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsMemoori
 
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...HostedbyConfluent
 
Hyderabad Call Girls Khairatabad ✨ 7001305949 ✨ Cheap Price Your Budget
Hyderabad Call Girls Khairatabad ✨ 7001305949 ✨ Cheap Price Your BudgetHyderabad Call Girls Khairatabad ✨ 7001305949 ✨ Cheap Price Your Budget
Hyderabad Call Girls Khairatabad ✨ 7001305949 ✨ Cheap Price Your BudgetEnjoy Anytime
 
Maximizing Board Effectiveness 2024 Webinar.pptx
Maximizing Board Effectiveness 2024 Webinar.pptxMaximizing Board Effectiveness 2024 Webinar.pptx
Maximizing Board Effectiveness 2024 Webinar.pptxOnBoard
 

Recently uploaded (20)

CloudStudio User manual (basic edition):
CloudStudio User manual (basic edition):CloudStudio User manual (basic edition):
CloudStudio User manual (basic edition):
 
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptxMaking_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
 
IAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI SolutionsIAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI Solutions
 
Enhancing Worker Digital Experience: A Hands-on Workshop for Partners
Enhancing Worker Digital Experience: A Hands-on Workshop for PartnersEnhancing Worker Digital Experience: A Hands-on Workshop for Partners
Enhancing Worker Digital Experience: A Hands-on Workshop for Partners
 
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
 
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptxE-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
 
How to Remove Document Management Hurdles with X-Docs?
How to Remove Document Management Hurdles with X-Docs?How to Remove Document Management Hurdles with X-Docs?
How to Remove Document Management Hurdles with X-Docs?
 
Next-generation AAM aircraft unveiled by Supernal, S-A2
Next-generation AAM aircraft unveiled by Supernal, S-A2Next-generation AAM aircraft unveiled by Supernal, S-A2
Next-generation AAM aircraft unveiled by Supernal, S-A2
 
Slack Application Development 101 Slides
Slack Application Development 101 SlidesSlack Application Development 101 Slides
Slack Application Development 101 Slides
 
08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking Men08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking Men
 
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure serviceWhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
 
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
 
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
 
Artificial intelligence in the post-deep learning era
Artificial intelligence in the post-deep learning eraArtificial intelligence in the post-deep learning era
Artificial intelligence in the post-deep learning era
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day Presentation
 
AI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsAI as an Interface for Commercial Buildings
AI as an Interface for Commercial Buildings
 
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
 
Hyderabad Call Girls Khairatabad ✨ 7001305949 ✨ Cheap Price Your Budget
Hyderabad Call Girls Khairatabad ✨ 7001305949 ✨ Cheap Price Your BudgetHyderabad Call Girls Khairatabad ✨ 7001305949 ✨ Cheap Price Your Budget
Hyderabad Call Girls Khairatabad ✨ 7001305949 ✨ Cheap Price Your Budget
 
Maximizing Board Effectiveness 2024 Webinar.pptx
Maximizing Board Effectiveness 2024 Webinar.pptxMaximizing Board Effectiveness 2024 Webinar.pptx
Maximizing Board Effectiveness 2024 Webinar.pptx
 

Building an invite-only microsite with Next.js & Airtable - ReactJS Milano

  • 3. OUR MISSION TODAY: LET'S BUILD AN INVITE-ONLY WEBSITE! loige 3
  • 4. 15 SECONDS DEMO ⏱ loige 4
  • 6. (SELF-IMPOSED) REQUIREMENTS Iterate quickly Simple to host, maintain and update Lightweight backend Non-techy people can easily access the data 💸Cheap (or even FREE) hosting! loige 6
  • 7. LET ME INTRODUCE MYSELF FIRST... 👋I'm Luciano ( 🍕🍝 ) Senior Architect @ fourTheorem (Dublin ) nodejsdp.link 📔Co-Author of Node.js Design Patterns 👉 Let's connect! (blog) (twitter) (twitch) (github) loige.co @loige loige lmammino 7
  • 8. ALWAYS RE-IMAGINING WE ARE A PIONEERING TECHNOLOGY CONSULTANCY FOCUSED ON AWS AND SERVERLESS | | Accelerated Serverless AI as a Service Platform Modernisation loige ✉Reach out to us at 😇We are always looking for talent: hello@fourTheorem.com fth.link/careers 8
  • 9. 📒AGENDA Choosing the tech stack The data flow Using Airtable as a database Creating APIs with Next.js and Vercel Creating custom React Hooks Using user interaction to update the data Security considerations loige 9
  • 11. MAKING A NEXT.JS PRIVATE Every guest should see something different People without an invite code should not be able to access any content loige 11
  • 13. STEP 1. LET'S ORGANIZE THE DATA IN AIRTABLE loige 13
  • 14. MANAGING DATA Invite codes are UUIDs Every record contains the information for every guest (name, etc) loige 14
  • 15. AIRTABLE LINGO loige Base (project) Table Records Fields 15
  • 16. STEP 2. NEXT.JS SCAFFOLDING AND RETRIEVING INVITES loige 16
  • 17. NEW NEXT.JS PROJECTS loige npx create-next-app@12.2 --typescript --use-npm (used Next.js 12.2) 17
  • 18. INVITE TYPE loige export interface Invite { code: string, name: string, favouriteColor: string, weapon: string, coming?: boolean, } 18
  • 19. AIRTABLE SDK loige npm i --save airtable export AIRTABLE_API_KEY="put your api key here" export AIRTABLE_BASE_ID="put your base id here" 19
  • 23. // utils/airtable.ts import Airtable from 'airtable' import { Invite } from '../types/invite' if (!process.env.AIRTABLE_API_KEY) { throw new Error('AIRTABLE_API_KEY is not set') } if (!process.env.AIRTABLE_BASE_ID) { throw new Error('AIRTABLE_BASE_ID is not set') } const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) const base = airtable.base(process.env.AIRTABLE_BASE_ID) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // utils/airtable.ts 1 2 import Airtable from 'airtable' 3 import { Invite } from '../types/invite' 4 5 if (!process.env.AIRTABLE_API_KEY) { 6 throw new Error('AIRTABLE_API_KEY is not set') 7 } 8 if (!process.env.AIRTABLE_BASE_ID) { 9 throw new Error('AIRTABLE_BASE_ID is not set') 10 } 11 12 const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) 13 const base = airtable.base(process.env.AIRTABLE_BASE_ID) 14 import Airtable from 'airtable' import { Invite } from '../types/invite' // utils/airtable.ts 1 2 3 4 5 if (!process.env.AIRTABLE_API_KEY) { 6 throw new Error('AIRTABLE_API_KEY is not set') 7 } 8 if (!process.env.AIRTABLE_BASE_ID) { 9 throw new Error('AIRTABLE_BASE_ID is not set') 10 } 11 12 const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) 13 const base = airtable.base(process.env.AIRTABLE_BASE_ID) 14 if (!process.env.AIRTABLE_API_KEY) { throw new Error('AIRTABLE_API_KEY is not set') } if (!process.env.AIRTABLE_BASE_ID) { throw new Error('AIRTABLE_BASE_ID is not set') } // utils/airtable.ts 1 2 import Airtable from 'airtable' 3 import { Invite } from '../types/invite' 4 5 6 7 8 9 10 11 12 const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) 13 const base = airtable.base(process.env.AIRTABLE_BASE_ID) 14 const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) const base = airtable.base(process.env.AIRTABLE_BASE_ID) // utils/airtable.ts 1 2 import Airtable from 'airtable' 3 import { Invite } from '../types/invite' 4 5 if (!process.env.AIRTABLE_API_KEY) { 6 throw new Error('AIRTABLE_API_KEY is not set') 7 } 8 if (!process.env.AIRTABLE_BASE_ID) { 9 throw new Error('AIRTABLE_BASE_ID is not set') 10 } 11 12 13 14 // utils/airtable.ts import Airtable from 'airtable' import { Invite } from '../types/invite' if (!process.env.AIRTABLE_API_KEY) { throw new Error('AIRTABLE_API_KEY is not set') } if (!process.env.AIRTABLE_BASE_ID) { throw new Error('AIRTABLE_BASE_ID is not set') } const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) const base = airtable.base(process.env.AIRTABLE_BASE_ID) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 loige 23
  • 24. export function getInvite (inviteCode: string): Promise<Invite> { return new Promise((resolve, reject) => { base('invites') .select({ filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape maxRecords: 1 }) .firstPage((err, records) => { if (err) { console.error(err) return reject(err) } if (!records || records.length === 0) { return reject(new Error('Invite not found')) } resolve({ code: String(records[0].fields.invite), name: String(records[0].fields.name), favouriteColor: String(records[0].fields.favouriteColor), weapon: String(records[0].fields.weapon), coming: typeof records[0].fields.coming === 'undefined' ? undefined : records[0].fields.coming === 'yes' }) }) }) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export function getInvite (inviteCode: string): Promise<Invite> { } 1 return new Promise((resolve, reject) => { 2 base('invites') 3 .select({ 4 filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape 5 maxRecords: 1 6 }) 7 .firstPage((err, records) => { 8 if (err) { 9 console.error(err) 10 return reject(err) 11 } 12 13 if (!records || records.length === 0) { 14 return reject(new Error('Invite not found')) 15 } 16 17 resolve({ 18 code: String(records[0].fields.invite), 19 name: String(records[0].fields.name), 20 favouriteColor: String(records[0].fields.favouriteColor), 21 weapon: String(records[0].fields.weapon), 22 coming: typeof records[0].fields.coming === 'undefined' 23 ? undefined 24 : records[0].fields.coming === 'yes' 25 }) 26 }) 27 }) 28 29 return new Promise((resolve, reject) => { }) export function getInvite (inviteCode: string): Promise<Invite> { 1 2 base('invites') 3 .select({ 4 filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape 5 maxRecords: 1 6 }) 7 .firstPage((err, records) => { 8 if (err) { 9 console.error(err) 10 return reject(err) 11 } 12 13 if (!records || records.length === 0) { 14 return reject(new Error('Invite not found')) 15 } 16 17 resolve({ 18 code: String(records[0].fields.invite), 19 name: String(records[0].fields.name), 20 favouriteColor: String(records[0].fields.favouriteColor), 21 weapon: String(records[0].fields.weapon), 22 coming: typeof records[0].fields.coming === 'undefined' 23 ? undefined 24 : records[0].fields.coming === 'yes' 25 }) 26 }) 27 28 } 29 base('invites') export function getInvite (inviteCode: string): Promise<Invite> { 1 return new Promise((resolve, reject) => { 2 3 .select({ 4 filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape 5 maxRecords: 1 6 }) 7 .firstPage((err, records) => { 8 if (err) { 9 console.error(err) 10 return reject(err) 11 } 12 13 if (!records || records.length === 0) { 14 return reject(new Error('Invite not found')) 15 } 16 17 resolve({ 18 code: String(records[0].fields.invite), 19 name: String(records[0].fields.name), 20 favouriteColor: String(records[0].fields.favouriteColor), 21 weapon: String(records[0].fields.weapon), 22 coming: typeof records[0].fields.coming === 'undefined' 23 ? undefined 24 : records[0].fields.coming === 'yes' 25 }) 26 }) 27 }) 28 } 29 .select({ filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape maxRecords: 1 }) export function getInvite (inviteCode: string): Promise<Invite> { 1 return new Promise((resolve, reject) => { 2 base('invites') 3 4 5 6 7 .firstPage((err, records) => { 8 if (err) { 9 console.error(err) 10 return reject(err) 11 } 12 13 if (!records || records.length === 0) { 14 return reject(new Error('Invite not found')) 15 } 16 17 resolve({ 18 code: String(records[0].fields.invite), 19 name: String(records[0].fields.name), 20 favouriteColor: String(records[0].fields.favouriteColor), 21 weapon: String(records[0].fields.weapon), 22 coming: typeof records[0].fields.coming === 'undefined' 23 ? undefined 24 : records[0].fields.coming === 'yes' 25 }) 26 }) 27 }) 28 } 29 .firstPage((err, records) => { }) export function getInvite (inviteCode: string): Promise<Invite> { 1 return new Promise((resolve, reject) => { 2 base('invites') 3 .select({ 4 filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape 5 maxRecords: 1 6 }) 7 8 if (err) { 9 console.error(err) 10 return reject(err) 11 } 12 13 if (!records || records.length === 0) { 14 return reject(new Error('Invite not found')) 15 } 16 17 resolve({ 18 code: String(records[0].fields.invite), 19 name: String(records[0].fields.name), 20 favouriteColor: String(records[0].fields.favouriteColor), 21 weapon: String(records[0].fields.weapon), 22 coming: typeof records[0].fields.coming === 'undefined' 23 ? undefined 24 : records[0].fields.coming === 'yes' 25 }) 26 27 }) 28 } 29 if (err) { console.error(err) return reject(err) } if (!records || records.length === 0) { return reject(new Error('Invite not found')) } export function getInvite (inviteCode: string): Promise<Invite> { 1 return new Promise((resolve, reject) => { 2 base('invites') 3 .select({ 4 filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape 5 maxRecords: 1 6 }) 7 .firstPage((err, records) => { 8 9 10 11 12 13 14 15 16 17 resolve({ 18 code: String(records[0].fields.invite), 19 name: String(records[0].fields.name), 20 favouriteColor: String(records[0].fields.favouriteColor), 21 weapon: String(records[0].fields.weapon), 22 coming: typeof records[0].fields.coming === 'undefined' 23 ? undefined 24 : records[0].fields.coming === 'yes' 25 }) 26 }) 27 }) 28 } 29 resolve({ code: String(records[0].fields.invite), name: String(records[0].fields.name), favouriteColor: String(records[0].fields.favouriteColor), weapon: String(records[0].fields.weapon), coming: typeof records[0].fields.coming === 'undefined' ? undefined : records[0].fields.coming === 'yes' }) export function getInvite (inviteCode: string): Promise<Invite> { 1 return new Promise((resolve, reject) => { 2 base('invites') 3 .select({ 4 filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape 5 maxRecords: 1 6 }) 7 .firstPage((err, records) => { 8 if (err) { 9 console.error(err) 10 return reject(err) 11 } 12 13 if (!records || records.length === 0) { 14 return reject(new Error('Invite not found')) 15 } 16 17 18 19 20 21 22 23 24 25 26 }) 27 }) 28 } 29 export function getInvite (inviteCode: string): Promise<Invite> { return new Promise((resolve, reject) => { base('invites') .select({ filterByFormula: `{invite} = ${escape(inviteCode)}`, // <- we'll talk more about escape maxRecords: 1 }) .firstPage((err, records) => { if (err) { console.error(err) return reject(err) } if (!records || records.length === 0) { return reject(new Error('Invite not found')) } resolve({ code: String(records[0].fields.invite), name: String(records[0].fields.name), favouriteColor: String(records[0].fields.favouriteColor), weapon: String(records[0].fields.weapon), coming: typeof records[0].fields.coming === 'undefined' ? undefined : records[0].fields.coming === 'yes' }) }) }) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 loige 24
  • 25. STEP 3. NEXT.JS INVITE API loige 25
  • 26. // pages/api/hello.ts -> <host>/api/hello import type { NextApiRequest, NextApiResponse } from 'next' export default async function handler ( req: NextApiRequest, res: NextApiResponse<{ message: string }> ) { return res.status(200).json({ message: 'Hello World' }) } 1 2 3 4 5 6 7 8 9 10 // pages/api/hello.ts -> <host>/api/hello 1 2 import type { NextApiRequest, NextApiResponse } from 'next' 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<{ message: string }> 7 ) { 8 return res.status(200).json({ message: 'Hello World' }) 9 } 10 import type { NextApiRequest, NextApiResponse } from 'next' // pages/api/hello.ts -> <host>/api/hello 1 2 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<{ message: string }> 7 ) { 8 return res.status(200).json({ message: 'Hello World' }) 9 } 10 export default async function handler ( req: NextApiRequest, res: NextApiResponse<{ message: string }> ) { } // pages/api/hello.ts -> <host>/api/hello 1 2 import type { NextApiRequest, NextApiResponse } from 'next' 3 4 5 6 7 8 return res.status(200).json({ message: 'Hello World' }) 9 10 return res.status(200).json({ message: 'Hello World' }) // pages/api/hello.ts -> <host>/api/hello 1 2 import type { NextApiRequest, NextApiResponse } from 'next' 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<{ message: string }> 7 ) { 8 9 } 10 // pages/api/hello.ts -> <host>/api/hello import type { NextApiRequest, NextApiResponse } from 'next' export default async function handler ( req: NextApiRequest, res: NextApiResponse<{ message: string }> ) { return res.status(200).json({ message: 'Hello World' }) } 1 2 3 4 5 6 7 8 9 10 APIS WITH NEXT.JS Files inside pages/api are API endpoints loige 26
  • 27. // pages/api/invite.ts import { InviteResponse } from '../../types/invite' import { getInvite } from '../../utils/airtable' export default async function handler ( req: NextApiRequest, res: NextApiResponse<InviteResponse | { error: string }> ) { if (req.method !== 'GET') { return res.status(405).json({ error: 'Method Not Allowed' }) } if (!req.query.code) { return res.status(400).json({ error: 'Missing invite code' }) } const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code try { const invite = await getInvite(code) res.status(200).json({ invite }) } catch (err) { if ((err as Error).message === 'Invite not found') { return res.status(401).json({ error: 'Invite not found' }) } res.status(500).json({ error: 'Internal server error' }) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // pages/api/invite.ts 1 import { InviteResponse } from '../../types/invite' 2 import { getInvite } from '../../utils/airtable' 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<InviteResponse | { error: string }> 7 ) { 8 if (req.method !== 'GET') { 9 return res.status(405).json({ error: 'Method Not Allowed' }) 10 } 11 if (!req.query.code) { 12 return res.status(400).json({ error: 'Missing invite code' }) 13 } 14 15 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 16 17 try { 18 const invite = await getInvite(code) 19 res.status(200).json({ invite }) 20 } catch (err) { 21 if ((err as Error).message === 'Invite not found') { 22 return res.status(401).json({ error: 'Invite not found' }) 23 } 24 res.status(500).json({ error: 'Internal server error' }) 25 } 26 } 27 import { InviteResponse } from '../../types/invite' import { getInvite } from '../../utils/airtable' // pages/api/invite.ts 1 2 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<InviteResponse | { error: string }> 7 ) { 8 if (req.method !== 'GET') { 9 return res.status(405).json({ error: 'Method Not Allowed' }) 10 } 11 if (!req.query.code) { 12 return res.status(400).json({ error: 'Missing invite code' }) 13 } 14 15 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 16 17 try { 18 const invite = await getInvite(code) 19 res.status(200).json({ invite }) 20 } catch (err) { 21 if ((err as Error).message === 'Invite not found') { 22 return res.status(401).json({ error: 'Invite not found' }) 23 } 24 res.status(500).json({ error: 'Internal server error' }) 25 } 26 } 27 export default async function handler ( req: NextApiRequest, res: NextApiResponse<InviteResponse | { error: string }> ) { } // pages/api/invite.ts 1 import { InviteResponse } from '../../types/invite' 2 import { getInvite } from '../../utils/airtable' 3 4 5 6 7 8 if (req.method !== 'GET') { 9 return res.status(405).json({ error: 'Method Not Allowed' }) 10 } 11 if (!req.query.code) { 12 return res.status(400).json({ error: 'Missing invite code' }) 13 } 14 15 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 16 17 try { 18 const invite = await getInvite(code) 19 res.status(200).json({ invite }) 20 } catch (err) { 21 if ((err as Error).message === 'Invite not found') { 22 return res.status(401).json({ error: 'Invite not found' }) 23 } 24 res.status(500).json({ error: 'Internal server error' }) 25 } 26 27 if (req.method !== 'GET') { return res.status(405).json({ error: 'Method Not Allowed' }) } if (!req.query.code) { return res.status(400).json({ error: 'Missing invite code' }) } // pages/api/invite.ts 1 import { InviteResponse } from '../../types/invite' 2 import { getInvite } from '../../utils/airtable' 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<InviteResponse | { error: string }> 7 ) { 8 9 10 11 12 13 14 15 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 16 17 try { 18 const invite = await getInvite(code) 19 res.status(200).json({ invite }) 20 } catch (err) { 21 if ((err as Error).message === 'Invite not found') { 22 return res.status(401).json({ error: 'Invite not found' }) 23 } 24 res.status(500).json({ error: 'Internal server error' }) 25 } 26 } 27 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code // pages/api/invite.ts 1 import { InviteResponse } from '../../types/invite' 2 import { getInvite } from '../../utils/airtable' 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<InviteResponse | { error: string }> 7 ) { 8 if (req.method !== 'GET') { 9 return res.status(405).json({ error: 'Method Not Allowed' }) 10 } 11 if (!req.query.code) { 12 return res.status(400).json({ error: 'Missing invite code' }) 13 } 14 15 16 17 try { 18 const invite = await getInvite(code) 19 res.status(200).json({ invite }) 20 } catch (err) { 21 if ((err as Error).message === 'Invite not found') { 22 return res.status(401).json({ error: 'Invite not found' }) 23 } 24 res.status(500).json({ error: 'Internal server error' }) 25 } 26 } 27 try { } // pages/api/invite.ts 1 import { InviteResponse } from '../../types/invite' 2 import { getInvite } from '../../utils/airtable' 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<InviteResponse | { error: string }> 7 ) { 8 if (req.method !== 'GET') { 9 return res.status(405).json({ error: 'Method Not Allowed' }) 10 } 11 if (!req.query.code) { 12 return res.status(400).json({ error: 'Missing invite code' }) 13 } 14 15 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 16 17 18 const invite = await getInvite(code) 19 res.status(200).json({ invite }) 20 } catch (err) { 21 if ((err as Error).message === 'Invite not found') { 22 return res.status(401).json({ error: 'Invite not found' }) 23 } 24 res.status(500).json({ error: 'Internal server error' }) 25 26 } 27 const invite = await getInvite(code) res.status(200).json({ invite }) // pages/api/invite.ts 1 import { InviteResponse } from '../../types/invite' 2 import { getInvite } from '../../utils/airtable' 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<InviteResponse | { error: string }> 7 ) { 8 if (req.method !== 'GET') { 9 return res.status(405).json({ error: 'Method Not Allowed' }) 10 } 11 if (!req.query.code) { 12 return res.status(400).json({ error: 'Missing invite code' }) 13 } 14 15 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 16 17 try { 18 19 20 } catch (err) { 21 if ((err as Error).message === 'Invite not found') { 22 return res.status(401).json({ error: 'Invite not found' }) 23 } 24 res.status(500).json({ error: 'Internal server error' }) 25 } 26 } 27 if ((err as Error).message === 'Invite not found') { return res.status(401).json({ error: 'Invite not found' }) } res.status(500).json({ error: 'Internal server error' }) // pages/api/invite.ts 1 import { InviteResponse } from '../../types/invite' 2 import { getInvite } from '../../utils/airtable' 3 4 export default async function handler ( 5 req: NextApiRequest, 6 res: NextApiResponse<InviteResponse | { error: string }> 7 ) { 8 if (req.method !== 'GET') { 9 return res.status(405).json({ error: 'Method Not Allowed' }) 10 } 11 if (!req.query.code) { 12 return res.status(400).json({ error: 'Missing invite code' }) 13 } 14 15 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 16 17 try { 18 const invite = await getInvite(code) 19 res.status(200).json({ invite }) 20 } catch (err) { 21 22 23 24 25 } 26 } 27 // pages/api/invite.ts import { InviteResponse } from '../../types/invite' import { getInvite } from '../../utils/airtable' export default async function handler ( req: NextApiRequest, res: NextApiResponse<InviteResponse | { error: string }> ) { if (req.method !== 'GET') { return res.status(405).json({ error: 'Method Not Allowed' }) } if (!req.query.code) { return res.status(400).json({ error: 'Missing invite code' }) } const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code try { const invite = await getInvite(code) res.status(200).json({ invite }) } catch (err) { if ((err as Error).message === 'Invite not found') { return res.status(401).json({ error: 'Invite not found' }) } res.status(500).json({ error: 'Internal server error' }) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 loige 27
  • 29. STEP 4. INVITE VALIDATION IN REACT loige 29
  • 30. ATTACK PLAN 🤺 When the SPA loads: We grab the invite code from the URL We call the invite API with the code ✅If it's valid, we render the content ❌If it's invalid, we render an error loige 30
  • 31. HOW DO WE MANAGE THIS DATA FETCHING LIFECYCLE? 😰 In-line in the top-level component (App)? In a Context provider? In a specialized React Hook? loige 31
  • 32. HOW CAN WE CREATE A CUSTOM REACT HOOK? 🤓 A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks It doesn’t need to have a specific signature Inside the function, all the common rules of hooks apply: Only call Hooks at the top level Don’t call Hooks inside loops, conditions, or nested functions reactjs.org/docs/hooks-custom.html loige 32
  • 33. // components/hooks/useInvite.tsx import { useState, useEffect } from 'react' import { InviteResponse } from '../../types/invite' async function fetchInvite (code: string): Promise<InviteResponse> { // makes a fetch request to the invite api (elided for brevity) } export default function useInvite (): [InviteResponse | null, string | null] { const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) const [error, setError] = useState<string | null>(null) useEffect(() => { const url = new URL(window.location.toString()) const code = url.searchParams.get('code') if (!code) { setError('No code provided') } else { fetchInvite(code) .then(setInviteResponse) .catch(err => { setError(err.message) }) } }, []) return [inviteResponse, error] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 useEffect(() => { 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 if (!code) { 16 setError('No code provided') 17 } else { 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 } 24 }, []) 25 26 return [inviteResponse, error] 27 } 28 import { useState, useEffect } from 'react' import { InviteResponse } from '../../types/invite' // components/hooks/useInvite.tsx 1 2 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 useEffect(() => { 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 if (!code) { 16 setError('No code provided') 17 } else { 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 } 24 }, []) 25 26 return [inviteResponse, error] 27 } 28 async function fetchInvite (code: string): Promise<InviteResponse> { // makes a fetch request to the invite api (elided for brevity) } // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 4 5 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 useEffect(() => { 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 if (!code) { 16 setError('No code provided') 17 } else { 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 } 24 }, []) 25 26 return [inviteResponse, error] 27 } 28 export default function useInvite (): [InviteResponse | null, string | null] { } // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 useEffect(() => { 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 if (!code) { 16 setError('No code provided') 17 } else { 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 } 24 }, []) 25 26 return [inviteResponse, error] 27 28 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) const [error, setError] = useState<string | null>(null) // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 9 10 11 useEffect(() => { 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 if (!code) { 16 setError('No code provided') 17 } else { 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 } 24 }, []) 25 26 return [inviteResponse, error] 27 } 28 useEffect(() => { }, []) // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 if (!code) { 16 setError('No code provided') 17 } else { 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 } 24 25 26 return [inviteResponse, error] 27 } 28 const url = new URL(window.location.toString()) const code = url.searchParams.get('code') // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 useEffect(() => { 12 13 14 15 if (!code) { 16 setError('No code provided') 17 } else { 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 } 24 }, []) 25 26 return [inviteResponse, error] 27 } 28 if (!code) { } else { } // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 useEffect(() => { 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 16 setError('No code provided') 17 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 24 }, []) 25 26 return [inviteResponse, error] 27 } 28 setError('No code provided') // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 useEffect(() => { 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 if (!code) { 16 17 } else { 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 } 24 }, []) 25 26 return [inviteResponse, error] 27 } 28 fetchInvite(code) .then(setInviteResponse) .catch(err => { setError(err.message) }) // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 useEffect(() => { 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 if (!code) { 16 setError('No code provided') 17 } else { 18 19 20 21 22 23 } 24 }, []) 25 26 return [inviteResponse, error] 27 } 28 return [inviteResponse, error] // components/hooks/useInvite.tsx 1 import { useState, useEffect } from 'react' 2 import { InviteResponse } from '../../types/invite' 3 async function fetchInvite (code: string): Promise<InviteResponse> { 4 // makes a fetch request to the invite api (elided for brevity) 5 } 6 7 export default function useInvite (): [InviteResponse | null, string | null] { 8 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 9 const [error, setError] = useState<string | null>(null) 10 11 useEffect(() => { 12 const url = new URL(window.location.toString()) 13 const code = url.searchParams.get('code') 14 15 if (!code) { 16 setError('No code provided') 17 } else { 18 fetchInvite(code) 19 .then(setInviteResponse) 20 .catch(err => { 21 setError(err.message) 22 }) 23 } 24 }, []) 25 26 27 } 28 // components/hooks/useInvite.tsx import { useState, useEffect } from 'react' import { InviteResponse } from '../../types/invite' async function fetchInvite (code: string): Promise<InviteResponse> { // makes a fetch request to the invite api (elided for brevity) } export default function useInvite (): [InviteResponse | null, string | null] { const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) const [error, setError] = useState<string | null>(null) useEffect(() => { const url = new URL(window.location.toString()) const code = url.searchParams.get('code') if (!code) { setError('No code provided') } else { fetchInvite(code) .then(setInviteResponse) .catch(err => { setError(err.message) }) } }, []) return [inviteResponse, error] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 loige 33
  • 34. import React from 'react' import useInvite from './hooks/useInvite' export default function SomeExampleComponent () { const [inviteResponse, error] = useInvite() // there was an error if (error) { return <div>... some error happened</div> } // still loading the data from the backend if (!inviteResponse) { return <div>Loading ...</div> } // has the data! return <div> actual component markup when inviteResponse is available </div> } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React from 'react' import useInvite from './hooks/useInvite' 1 2 3 export default function SomeExampleComponent () { 4 const [inviteResponse, error] = useInvite() 5 6 // there was an error 7 if (error) { 8 return <div>... some error happened</div> 9 } 10 11 // still loading the data from the backend 12 if (!inviteResponse) { 13 return <div>Loading ...</div> 14 } 15 16 // has the data! 17 return <div> 18 actual component markup when inviteResponse is available 19 </div> 20 } 21 export default function SomeExampleComponent () { } import React from 'react' 1 import useInvite from './hooks/useInvite' 2 3 4 const [inviteResponse, error] = useInvite() 5 6 // there was an error 7 if (error) { 8 return <div>... some error happened</div> 9 } 10 11 // still loading the data from the backend 12 if (!inviteResponse) { 13 return <div>Loading ...</div> 14 } 15 16 // has the data! 17 return <div> 18 actual component markup when inviteResponse is available 19 </div> 20 21 const [inviteResponse, error] = useInvite() import React from 'react' 1 import useInvite from './hooks/useInvite' 2 3 export default function SomeExampleComponent () { 4 5 6 // there was an error 7 if (error) { 8 return <div>... some error happened</div> 9 } 10 11 // still loading the data from the backend 12 if (!inviteResponse) { 13 return <div>Loading ...</div> 14 } 15 16 // has the data! 17 return <div> 18 actual component markup when inviteResponse is available 19 </div> 20 } 21 // there was an error if (error) { return <div>... some error happened</div> } import React from 'react' 1 import useInvite from './hooks/useInvite' 2 3 export default function SomeExampleComponent () { 4 const [inviteResponse, error] = useInvite() 5 6 7 8 9 10 11 // still loading the data from the backend 12 if (!inviteResponse) { 13 return <div>Loading ...</div> 14 } 15 16 // has the data! 17 return <div> 18 actual component markup when inviteResponse is available 19 </div> 20 } 21 // still loading the data from the backend if (!inviteResponse) { return <div>Loading ...</div> } import React from 'react' 1 import useInvite from './hooks/useInvite' 2 3 export default function SomeExampleComponent () { 4 const [inviteResponse, error] = useInvite() 5 6 // there was an error 7 if (error) { 8 return <div>... some error happened</div> 9 } 10 11 12 13 14 15 16 // has the data! 17 return <div> 18 actual component markup when inviteResponse is available 19 </div> 20 } 21 // has the data! return <div> actual component markup when inviteResponse is available </div> import React from 'react' 1 import useInvite from './hooks/useInvite' 2 3 export default function SomeExampleComponent () { 4 const [inviteResponse, error] = useInvite() 5 6 // there was an error 7 if (error) { 8 return <div>... some error happened</div> 9 } 10 11 // still loading the data from the backend 12 if (!inviteResponse) { 13 return <div>Loading ...</div> 14 } 15 16 17 18 19 20 } 21 import React from 'react' import useInvite from './hooks/useInvite' export default function SomeExampleComponent () { const [inviteResponse, error] = useInvite() // there was an error if (error) { return <div>... some error happened</div> } // still loading the data from the backend if (!inviteResponse) { return <div>Loading ...</div> } // has the data! return <div> actual component markup when inviteResponse is available </div> } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 loige EXAMPLE USAGE: 34
  • 35. STEP 5. COLLECTING USER DATA loige 35
  • 36. CHANGES REQUIRED 🙎 Add the "coming" field in Airtable Add a new backend utility to update the "coming" field for a given invite Add a new endpoint to update the coming field for the current user Update the React hook the expose the update functionality loige 36
  • 37. loige New field ("yes", "no", or undefined) ADD THE "COMING" FIELD IN AIRTABLE 37
  • 38. // utils/airtable.ts import Airtable, { FieldSet, Record } from 'airtable' // ... export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> { // gets the raw record for a given invite, elided for brevity } export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> { const { id } = await getInviteRecord(inviteCode) return new Promise((resolve, reject) => { base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => { if (err) { return reject(err) } resolve() }) }) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // utils/airtable.ts 1 import Airtable, { FieldSet, Record } from 'airtable' 2 // ... 3 4 export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> { 5 // gets the raw record for a given invite, elided for brevity 6 } 7 8 export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> { 9 const { id } = await getInviteRecord(inviteCode) 10 11 return new Promise((resolve, reject) => { 12 base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => { 13 if (err) { 14 return reject(err) 15 } 16 17 resolve() 18 }) 19 }) 20 } 21 import Airtable, { FieldSet, Record } from 'airtable' export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> { // gets the raw record for a given invite, elided for brevity } // utils/airtable.ts 1 2 // ... 3 4 5 6 7 8 export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> { 9 const { id } = await getInviteRecord(inviteCode) 10 11 return new Promise((resolve, reject) => { 12 base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => { 13 if (err) { 14 return reject(err) 15 } 16 17 resolve() 18 }) 19 }) 20 } 21 export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> { } // utils/airtable.ts 1 import Airtable, { FieldSet, Record } from 'airtable' 2 // ... 3 4 export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> { 5 // gets the raw record for a given invite, elided for brevity 6 } 7 8 9 const { id } = await getInviteRecord(inviteCode) 10 11 return new Promise((resolve, reject) => { 12 base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => { 13 if (err) { 14 return reject(err) 15 } 16 17 resolve() 18 }) 19 }) 20 21 const { id } = await getInviteRecord(inviteCode) // utils/airtable.ts 1 import Airtable, { FieldSet, Record } from 'airtable' 2 // ... 3 4 export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> { 5 // gets the raw record for a given invite, elided for brevity 6 } 7 8 export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> { 9 10 11 return new Promise((resolve, reject) => { 12 base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => { 13 if (err) { 14 return reject(err) 15 } 16 17 resolve() 18 }) 19 }) 20 } 21 return new Promise((resolve, reject) => { }) // utils/airtable.ts 1 import Airtable, { FieldSet, Record } from 'airtable' 2 // ... 3 4 export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> { 5 // gets the raw record for a given invite, elided for brevity 6 } 7 8 export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> { 9 const { id } = await getInviteRecord(inviteCode) 10 11 12 base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => { 13 if (err) { 14 return reject(err) 15 } 16 17 resolve() 18 }) 19 20 } 21 base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => { }) // utils/airtable.ts 1 import Airtable, { FieldSet, Record } from 'airtable' 2 // ... 3 4 export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> { 5 // gets the raw record for a given invite, elided for brevity 6 } 7 8 export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> { 9 const { id } = await getInviteRecord(inviteCode) 10 11 return new Promise((resolve, reject) => { 12 13 if (err) { 14 return reject(err) 15 } 16 17 resolve() 18 19 }) 20 } 21 if (err) { return reject(err) } resolve() // utils/airtable.ts 1 import Airtable, { FieldSet, Record } from 'airtable' 2 // ... 3 4 export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> { 5 // gets the raw record for a given invite, elided for brevity 6 } 7 8 export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> { 9 const { id } = await getInviteRecord(inviteCode) 10 11 return new Promise((resolve, reject) => { 12 base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => { 13 14 15 16 17 18 }) 19 }) 20 } 21 // utils/airtable.ts import Airtable, { FieldSet, Record } from 'airtable' // ... export function getInviteRecord (inviteCode: string): Promise<Record<FieldSet>> { // gets the raw record for a given invite, elided for brevity } export async function updateRsvp (inviteCode: string, rsvp: boolean): Promise<void> { const { id } = await getInviteRecord(inviteCode) return new Promise((resolve, reject) => { base('invites').update(id, { coming: rsvp ? 'yes' : 'no' }, (err) => { if (err) { return reject(err) } resolve() }) }) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 loige RSVP UTILITY 38
  • 39. // pages/api/rsvp.ts type RequestBody = {coming?: boolean} export default async function handler ( req: NextApiRequest, res: NextApiResponse<{updated: boolean} | { error: string }> ) { if (req.method !== 'PUT') { return res.status(405).json({ error: 'Method Not Allowed' }) } if (!req.query.code) { return res.status(400).json({ error: 'Missing invite code' }) } const reqBody = req.body as RequestBody if (typeof reqBody.coming === 'undefined') { return res.status(400).json({ error: 'Missing `coming` field in body' }) } const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code try { await updateRsvp(code, reqBody.coming) return res.status(200).json({ updated: true }) } catch (err) { if ((err as Error).message === 'Invite not found') { return res.status(401).json({ error: 'Invite not found' }) } res.status(500).json({ error: 'Internal server error' }) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // pages/api/rsvp.ts 1 type RequestBody = {coming?: boolean} 2 3 export default async function handler ( 4 req: NextApiRequest, 5 res: NextApiResponse<{updated: boolean} | { error: string }> 6 ) { 7 if (req.method !== 'PUT') { 8 return res.status(405).json({ error: 'Method Not Allowed' }) 9 } 10 if (!req.query.code) { 11 return res.status(400).json({ error: 'Missing invite code' }) 12 } 13 const reqBody = req.body as RequestBody 14 if (typeof reqBody.coming === 'undefined') { 15 return res.status(400).json({ error: 'Missing `coming` field in body' }) 16 } 17 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 18 19 try { 20 await updateRsvp(code, reqBody.coming) 21 return res.status(200).json({ updated: true }) 22 } catch (err) { 23 if ((err as Error).message === 'Invite not found') { 24 return res.status(401).json({ error: 'Invite not found' }) 25 } 26 res.status(500).json({ error: 'Internal server error' }) 27 } 28 } 29 type RequestBody = {coming?: boolean} // pages/api/rsvp.ts 1 2 3 export default async function handler ( 4 req: NextApiRequest, 5 res: NextApiResponse<{updated: boolean} | { error: string }> 6 ) { 7 if (req.method !== 'PUT') { 8 return res.status(405).json({ error: 'Method Not Allowed' }) 9 } 10 if (!req.query.code) { 11 return res.status(400).json({ error: 'Missing invite code' }) 12 } 13 const reqBody = req.body as RequestBody 14 if (typeof reqBody.coming === 'undefined') { 15 return res.status(400).json({ error: 'Missing `coming` field in body' }) 16 } 17 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 18 19 try { 20 await updateRsvp(code, reqBody.coming) 21 return res.status(200).json({ updated: true }) 22 } catch (err) { 23 if ((err as Error).message === 'Invite not found') { 24 return res.status(401).json({ error: 'Invite not found' }) 25 } 26 res.status(500).json({ error: 'Internal server error' }) 27 } 28 } 29 export default async function handler ( req: NextApiRequest, res: NextApiResponse<{updated: boolean} | { error: string }> ) { } // pages/api/rsvp.ts 1 type RequestBody = {coming?: boolean} 2 3 4 5 6 7 if (req.method !== 'PUT') { 8 return res.status(405).json({ error: 'Method Not Allowed' }) 9 } 10 if (!req.query.code) { 11 return res.status(400).json({ error: 'Missing invite code' }) 12 } 13 const reqBody = req.body as RequestBody 14 if (typeof reqBody.coming === 'undefined') { 15 return res.status(400).json({ error: 'Missing `coming` field in body' }) 16 } 17 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 18 19 try { 20 await updateRsvp(code, reqBody.coming) 21 return res.status(200).json({ updated: true }) 22 } catch (err) { 23 if ((err as Error).message === 'Invite not found') { 24 return res.status(401).json({ error: 'Invite not found' }) 25 } 26 res.status(500).json({ error: 'Internal server error' }) 27 } 28 29 if (req.method !== 'PUT') { return res.status(405).json({ error: 'Method Not Allowed' }) } if (!req.query.code) { return res.status(400).json({ error: 'Missing invite code' }) } // pages/api/rsvp.ts 1 type RequestBody = {coming?: boolean} 2 3 export default async function handler ( 4 req: NextApiRequest, 5 res: NextApiResponse<{updated: boolean} | { error: string }> 6 ) { 7 8 9 10 11 12 13 const reqBody = req.body as RequestBody 14 if (typeof reqBody.coming === 'undefined') { 15 return res.status(400).json({ error: 'Missing `coming` field in body' }) 16 } 17 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 18 19 try { 20 await updateRsvp(code, reqBody.coming) 21 return res.status(200).json({ updated: true }) 22 } catch (err) { 23 if ((err as Error).message === 'Invite not found') { 24 return res.status(401).json({ error: 'Invite not found' }) 25 } 26 res.status(500).json({ error: 'Internal server error' }) 27 } 28 } 29 const reqBody = req.body as RequestBody if (typeof reqBody.coming === 'undefined') { return res.status(400).json({ error: 'Missing `coming` field in body' }) } // pages/api/rsvp.ts 1 type RequestBody = {coming?: boolean} 2 3 export default async function handler ( 4 req: NextApiRequest, 5 res: NextApiResponse<{updated: boolean} | { error: string }> 6 ) { 7 if (req.method !== 'PUT') { 8 return res.status(405).json({ error: 'Method Not Allowed' }) 9 } 10 if (!req.query.code) { 11 return res.status(400).json({ error: 'Missing invite code' }) 12 } 13 14 15 16 17 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 18 19 try { 20 await updateRsvp(code, reqBody.coming) 21 return res.status(200).json({ updated: true }) 22 } catch (err) { 23 if ((err as Error).message === 'Invite not found') { 24 return res.status(401).json({ error: 'Invite not found' }) 25 } 26 res.status(500).json({ error: 'Internal server error' }) 27 } 28 } 29 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code // pages/api/rsvp.ts 1 type RequestBody = {coming?: boolean} 2 3 export default async function handler ( 4 req: NextApiRequest, 5 res: NextApiResponse<{updated: boolean} | { error: string }> 6 ) { 7 if (req.method !== 'PUT') { 8 return res.status(405).json({ error: 'Method Not Allowed' }) 9 } 10 if (!req.query.code) { 11 return res.status(400).json({ error: 'Missing invite code' }) 12 } 13 const reqBody = req.body as RequestBody 14 if (typeof reqBody.coming === 'undefined') { 15 return res.status(400).json({ error: 'Missing `coming` field in body' }) 16 } 17 18 19 try { 20 await updateRsvp(code, reqBody.coming) 21 return res.status(200).json({ updated: true }) 22 } catch (err) { 23 if ((err as Error).message === 'Invite not found') { 24 return res.status(401).json({ error: 'Invite not found' }) 25 } 26 res.status(500).json({ error: 'Internal server error' }) 27 } 28 } 29 try { } catch (err) { } // pages/api/rsvp.ts 1 type RequestBody = {coming?: boolean} 2 3 export default async function handler ( 4 req: NextApiRequest, 5 res: NextApiResponse<{updated: boolean} | { error: string }> 6 ) { 7 if (req.method !== 'PUT') { 8 return res.status(405).json({ error: 'Method Not Allowed' }) 9 } 10 if (!req.query.code) { 11 return res.status(400).json({ error: 'Missing invite code' }) 12 } 13 const reqBody = req.body as RequestBody 14 if (typeof reqBody.coming === 'undefined') { 15 return res.status(400).json({ error: 'Missing `coming` field in body' }) 16 } 17 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 18 19 20 await updateRsvp(code, reqBody.coming) 21 return res.status(200).json({ updated: true }) 22 23 if ((err as Error).message === 'Invite not found') { 24 return res.status(401).json({ error: 'Invite not found' }) 25 } 26 res.status(500).json({ error: 'Internal server error' }) 27 28 } 29 await updateRsvp(code, reqBody.coming) return res.status(200).json({ updated: true }) // pages/api/rsvp.ts 1 type RequestBody = {coming?: boolean} 2 3 export default async function handler ( 4 req: NextApiRequest, 5 res: NextApiResponse<{updated: boolean} | { error: string }> 6 ) { 7 if (req.method !== 'PUT') { 8 return res.status(405).json({ error: 'Method Not Allowed' }) 9 } 10 if (!req.query.code) { 11 return res.status(400).json({ error: 'Missing invite code' }) 12 } 13 const reqBody = req.body as RequestBody 14 if (typeof reqBody.coming === 'undefined') { 15 return res.status(400).json({ error: 'Missing `coming` field in body' }) 16 } 17 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 18 19 try { 20 21 22 } catch (err) { 23 if ((err as Error).message === 'Invite not found') { 24 return res.status(401).json({ error: 'Invite not found' }) 25 } 26 res.status(500).json({ error: 'Internal server error' }) 27 } 28 } 29 if ((err as Error).message === 'Invite not found') { return res.status(401).json({ error: 'Invite not found' }) } res.status(500).json({ error: 'Internal server error' }) // pages/api/rsvp.ts 1 type RequestBody = {coming?: boolean} 2 3 export default async function handler ( 4 req: NextApiRequest, 5 res: NextApiResponse<{updated: boolean} | { error: string }> 6 ) { 7 if (req.method !== 'PUT') { 8 return res.status(405).json({ error: 'Method Not Allowed' }) 9 } 10 if (!req.query.code) { 11 return res.status(400).json({ error: 'Missing invite code' }) 12 } 13 const reqBody = req.body as RequestBody 14 if (typeof reqBody.coming === 'undefined') { 15 return res.status(400).json({ error: 'Missing `coming` field in body' }) 16 } 17 const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code 18 19 try { 20 await updateRsvp(code, reqBody.coming) 21 return res.status(200).json({ updated: true }) 22 } catch (err) { 23 24 25 26 27 } 28 } 29 // pages/api/rsvp.ts type RequestBody = {coming?: boolean} export default async function handler ( req: NextApiRequest, res: NextApiResponse<{updated: boolean} | { error: string }> ) { if (req.method !== 'PUT') { return res.status(405).json({ error: 'Method Not Allowed' }) } if (!req.query.code) { return res.status(400).json({ error: 'Missing invite code' }) } const reqBody = req.body as RequestBody if (typeof reqBody.coming === 'undefined') { return res.status(400).json({ error: 'Missing `coming` field in body' }) } const code = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code try { await updateRsvp(code, reqBody.coming) return res.status(200).json({ updated: true }) } catch (err) { if ((err as Error).message === 'Invite not found') { return res.status(401).json({ error: 'Invite not found' }) } res.status(500).json({ error: 'Internal server error' }) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 loige RSVP API ENDPOINT 39
  • 40. // components/hooks/useInvite.tsx // ... interface HookResult { inviteResponse: InviteResponse | null, error: string | null, updating: boolean, updateRsvp: (coming: boolean) => Promise<void> } async function updateRsvpRequest (code: string, coming: boolean): Promise<void> { // Helper function that uses fetch to invoke the rsvp API endpoint (elided) } 1 2 3 4 5 6 7 8 9 10 11 12 13 // components/hooks/useInvite.tsx // ... 1 2 3 interface HookResult { 4 inviteResponse: InviteResponse | null, 5 error: string | null, 6 updating: boolean, 7 updateRsvp: (coming: boolean) => Promise<void> 8 } 9 10 async function updateRsvpRequest (code: string, coming: boolean): Promise<void> { 11 // Helper function that uses fetch to invoke the rsvp API endpoint (elided) 12 } 13 interface HookResult { inviteResponse: InviteResponse | null, error: string | null, updating: boolean, updateRsvp: (coming: boolean) => Promise<void> } // components/hooks/useInvite.tsx 1 // ... 2 3 4 5 6 7 8 9 10 async function updateRsvpRequest (code: string, coming: boolean): Promise<void> { 11 // Helper function that uses fetch to invoke the rsvp API endpoint (elided) 12 } 13 // components/hooks/useInvite.tsx // ... interface HookResult { inviteResponse: InviteResponse | null, error: string | null, updating: boolean, updateRsvp: (coming: boolean) => Promise<void> } async function updateRsvpRequest (code: string, coming: boolean): Promise<void> { // Helper function that uses fetch to invoke the rsvp API endpoint (elided) } 1 2 3 4 5 6 7 8 9 10 11 12 13 // components/hooks/useInvite.tsx // ... interface HookResult { inviteResponse: InviteResponse | null, error: string | null, updating: boolean, updateRsvp: (coming: boolean) => Promise<void> } async function updateRsvpRequest (code: string, coming: boolean): Promise<void> { // Helper function that uses fetch to invoke the rsvp API endpoint (elided) } 1 2 3 4 5 6 7 8 9 10 11 12 13 loige USEINVITE HOOK V2 40
  • 41. // ... export default function useInvite (): HookResult { const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) const [error, setError] = useState<string | null>(null) const [updating, setUpdating] = useState<boolean>(false) useEffect(() => { // load the invite using the code from URL, same as before }, []) async function updateRsvp (coming: boolean) { if (inviteResponse) { setUpdating(true) await updateRsvpRequest(inviteResponse.invite.code, coming) setInviteResponse({ ...inviteResponse, invite: { ...inviteResponse.invite, coming } }) setUpdating(false) } } return { inviteResponse, error, updating, updateRsvp } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 export default function useInvite (): HookResult { } // ... 1 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 async function updateRsvp (coming: boolean) { 11 if (inviteResponse) { 12 setUpdating(true) 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 setUpdating(false) 19 } 20 } 21 22 return { inviteResponse, error, updating, updateRsvp } 23 24 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) const [error, setError] = useState<string | null>(null) // ... 1 export default function useInvite (): HookResult { 2 3 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 async function updateRsvp (coming: boolean) { 11 if (inviteResponse) { 12 setUpdating(true) 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 setUpdating(false) 19 } 20 } 21 22 return { inviteResponse, error, updating, updateRsvp } 23 } 24 const [updating, setUpdating] = useState<boolean>(false) // ... 1 export default function useInvite (): HookResult { 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 async function updateRsvp (coming: boolean) { 11 if (inviteResponse) { 12 setUpdating(true) 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 setUpdating(false) 19 } 20 } 21 22 return { inviteResponse, error, updating, updateRsvp } 23 } 24 useEffect(() => { // load the invite using the code from URL, same as before }, []) // ... 1 export default function useInvite (): HookResult { 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 7 8 9 10 async function updateRsvp (coming: boolean) { 11 if (inviteResponse) { 12 setUpdating(true) 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 setUpdating(false) 19 } 20 } 21 22 return { inviteResponse, error, updating, updateRsvp } 23 } 24 async function updateRsvp (coming: boolean) { } // ... 1 export default function useInvite (): HookResult { 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 11 if (inviteResponse) { 12 setUpdating(true) 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 setUpdating(false) 19 } 20 21 22 return { inviteResponse, error, updating, updateRsvp } 23 } 24 if (inviteResponse) { } // ... 1 export default function useInvite (): HookResult { 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 async function updateRsvp (coming: boolean) { 11 12 setUpdating(true) 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 setUpdating(false) 19 20 } 21 22 return { inviteResponse, error, updating, updateRsvp } 23 } 24 setUpdating(true) // ... 1 export default function useInvite (): HookResult { 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 async function updateRsvp (coming: boolean) { 11 if (inviteResponse) { 12 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 setUpdating(false) 19 } 20 } 21 22 return { inviteResponse, error, updating, updateRsvp } 23 } 24 await updateRsvpRequest(inviteResponse.invite.code, coming) // ... 1 export default function useInvite (): HookResult { 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 async function updateRsvp (coming: boolean) { 11 if (inviteResponse) { 12 setUpdating(true) 13 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 setUpdating(false) 19 } 20 } 21 22 return { inviteResponse, error, updating, updateRsvp } 23 } 24 setInviteResponse({ ...inviteResponse, invite: { ...inviteResponse.invite, coming } }) // ... 1 export default function useInvite (): HookResult { 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 async function updateRsvp (coming: boolean) { 11 if (inviteResponse) { 12 setUpdating(true) 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 15 16 17 18 setUpdating(false) 19 } 20 } 21 22 return { inviteResponse, error, updating, updateRsvp } 23 } 24 setUpdating(false) // ... 1 export default function useInvite (): HookResult { 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 async function updateRsvp (coming: boolean) { 11 if (inviteResponse) { 12 setUpdating(true) 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 19 } 20 } 21 22 return { inviteResponse, error, updating, updateRsvp } 23 } 24 return { inviteResponse, error, updating, updateRsvp } // ... 1 export default function useInvite (): HookResult { 2 const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) 3 const [error, setError] = useState<string | null>(null) 4 const [updating, setUpdating] = useState<boolean>(false) 5 6 useEffect(() => { 7 // load the invite using the code from URL, same as before 8 }, []) 9 10 async function updateRsvp (coming: boolean) { 11 if (inviteResponse) { 12 setUpdating(true) 13 await updateRsvpRequest(inviteResponse.invite.code, coming) 14 setInviteResponse({ 15 ...inviteResponse, 16 invite: { ...inviteResponse.invite, coming } 17 }) 18 setUpdating(false) 19 } 20 } 21 22 23 } 24 // ... export default function useInvite (): HookResult { const [inviteResponse, setInviteResponse] = useState<InviteResponse | null>(null) const [error, setError] = useState<string | null>(null) const [updating, setUpdating] = useState<boolean>(false) useEffect(() => { // load the invite using the code from URL, same as before }, []) async function updateRsvp (coming: boolean) { if (inviteResponse) { setUpdating(true) await updateRsvpRequest(inviteResponse.invite.code, coming) setInviteResponse({ ...inviteResponse, invite: { ...inviteResponse.invite, coming } }) setUpdating(false) } } return { inviteResponse, error, updating, updateRsvp } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 loige 41
  • 42. import useInvite from './hooks/useInvite' export default function Home () { const { inviteResponse, error, updating, updateRsvp } = useInvite() if (error) { return <div>Duh! {error}</div> } if (!inviteResponse) { return <div>Loading...</div> } function onRsvpChange (e: ChangeEvent<HTMLInputElement>) { const coming = e.target.value === 'yes' updateRsvp(coming) } return (<fieldset disabled={updating}><legend>Are you coming?</legend> <label htmlFor="yes"> <input type="radio" id="yes" name="coming" value="yes" onChange={onRsvpChange} checked={inviteResponse.invite.coming === true} /> YES </label> <label htmlFor="no"> <input type="radio" id="no" name="coming" value="no" onChange={onRsvpChange} checked={inviteResponse.invite.coming === false} /> NO </label> </fieldset>) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import useInvite from './hooks/useInvite' 1 2 export default function Home () { 3 const { inviteResponse, error, updating, updateRsvp } = useInvite() 4 5 if (error) { return <div>Duh! {error}</div> } 6 if (!inviteResponse) { return <div>Loading...</div> } 7 8 function onRsvpChange (e: ChangeEvent<HTMLInputElement>) { 9 const coming = e.target.value === 'yes' 10 updateRsvp(coming) 11 } 12 13 return (<fieldset disabled={updating}><legend>Are you coming?</legend> 14 <label htmlFor="yes"> 15 <input type="radio" id="yes" name="coming" value="yes" 16 onChange={onRsvpChange} 17 checked={inviteResponse.invite.coming === true} 18 /> YES 19 </label> 20 <label htmlFor="no"> 21 <input type="radio" id="no" name="coming" value="no" 22 onChange={onRsvpChange} 23 checked={inviteResponse.invite.coming === false} 24 /> NO 25 </label> 26 </fieldset>) 27 } 28 export default function Home () { } import useInvite from './hooks/useInvite' 1 2 3 const { inviteResponse, error, updating, updateRsvp } = useInvite() 4 5 if (error) { return <div>Duh! {error}</div> } 6 if (!inviteResponse) { return <div>Loading...</div> } 7 8 function onRsvpChange (e: ChangeEvent<HTMLInputElement>) { 9 const coming = e.target.value === 'yes' 10 updateRsvp(coming) 11 } 12 13 return (<fieldset disabled={updating}><legend>Are you coming?</legend> 14 <label htmlFor="yes"> 15 <input type="radio" id="yes" name="coming" value="yes" 16 onChange={onRsvpChange} 17 checked={inviteResponse.invite.coming === true} 18 /> YES 19 </label> 20 <label htmlFor="no"> 21 <input type="radio" id="no" name="coming" value="no" 22 onChange={onRsvpChange} 23 checked={inviteResponse.invite.coming === false} 24 /> NO 25 </label> 26 </fieldset>) 27 28 const { inviteResponse, error, updating, updateRsvp } = useInvite() import useInvite from './hooks/useInvite' 1 2 export default function Home () { 3 4 5 if (error) { return <div>Duh! {error}</div> } 6 if (!inviteResponse) { return <div>Loading...</div> } 7 8 function onRsvpChange (e: ChangeEvent<HTMLInputElement>) { 9 const coming = e.target.value === 'yes' 10 updateRsvp(coming) 11 } 12 13 return (<fieldset disabled={updating}><legend>Are you coming?</legend> 14 <label htmlFor="yes"> 15 <input type="radio" id="yes" name="coming" value="yes" 16 onChange={onRsvpChange} 17 checked={inviteResponse.invite.coming === true} 18 /> YES 19 </label> 20 <label htmlFor="no"> 21 <input type="radio" id="no" name="coming" value="no" 22 onChange={onRsvpChange} 23 checked={inviteResponse.invite.coming === false} 24 /> NO 25 </label> 26 </fieldset>) 27 } 28 if (error) { return <div>Duh! {error}</div> } if (!inviteResponse) { return <div>Loading...</div> } import useInvite from './hooks/useInvite' 1 2 export default function Home () { 3 const { inviteResponse, error, updating, updateRsvp } = useInvite() 4 5 6 7 8 function onRsvpChange (e: ChangeEvent<HTMLInputElement>) { 9 const coming = e.target.value === 'yes' 10 updateRsvp(coming) 11 } 12 13 return (<fieldset disabled={updating}><legend>Are you coming?</legend> 14 <label htmlFor="yes"> 15 <input type="radio" id="yes" name="coming" value="yes" 16 onChange={onRsvpChange} 17 checked={inviteResponse.invite.coming === true} 18 /> YES 19 </label> 20 <label htmlFor="no"> 21 <input type="radio" id="no" name="coming" value="no" 22 onChange={onRsvpChange} 23 checked={inviteResponse.invite.coming === false} 24 /> NO 25 </label> 26 </fieldset>) 27 } 28 function onRsvpChange (e: ChangeEvent<HTMLInputElement>) { const coming = e.target.value === 'yes' updateRsvp(coming) } import useInvite from './hooks/useInvite' 1 2 export default function Home () { 3 const { inviteResponse, error, updating, updateRsvp } = useInvite() 4 5 if (error) { return <div>Duh! {error}</div> } 6 if (!inviteResponse) { return <div>Loading...</div> } 7 8 9 10 11 12 13 return (<fieldset disabled={updating}><legend>Are you coming?</legend> 14 <label htmlFor="yes"> 15 <input type="radio" id="yes" name="coming" value="yes" 16 onChange={onRsvpChange} 17 checked={inviteResponse.invite.coming === true} 18 /> YES 19 </label> 20 <label htmlFor="no"> 21 <input type="radio" id="no" name="coming" value="no" 22 onChange={onRsvpChange} 23 checked={inviteResponse.invite.coming === false} 24 /> NO 25 </label> 26 </fieldset>) 27 } 28 return (<fieldset disabled={updating}><legend>Are you coming?</legend> <label htmlFor="yes"> <input type="radio" id="yes" name="coming" value="yes" onChange={onRsvpChange} checked={inviteResponse.invite.coming === true} /> YES </label> <label htmlFor="no"> <input type="radio" id="no" name="coming" value="no" onChange={onRsvpChange} checked={inviteResponse.invite.coming === false} /> NO </label> </fieldset>) import useInvite from './hooks/useInvite' 1 2 export default function Home () { 3 const { inviteResponse, error, updating, updateRsvp } = useInvite() 4 5 if (error) { return <div>Duh! {error}</div> } 6 if (!inviteResponse) { return <div>Loading...</div> } 7 8 function onRsvpChange (e: ChangeEvent<HTMLInputElement>) { 9 const coming = e.target.value === 'yes' 10 updateRsvp(coming) 11 } 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 } 28 onChange={onRsvpChange} checked={inviteResponse.invite.coming === true} onChange={onRsvpChange} checked={inviteResponse.invite.coming === false} import useInvite from './hooks/useInvite' 1 2 export default function Home () { 3 const { inviteResponse, error, updating, updateRsvp } = useInvite() 4 5 if (error) { return <div>Duh! {error}</div> } 6 if (!inviteResponse) { return <div>Loading...</div> } 7 8 function onRsvpChange (e: ChangeEvent<HTMLInputElement>) { 9 const coming = e.target.value === 'yes' 10 updateRsvp(coming) 11 } 12 13 return (<fieldset disabled={updating}><legend>Are you coming?</legend> 14 <label htmlFor="yes"> 15 <input type="radio" id="yes" name="coming" value="yes" 16 17 18 /> YES 19 </label> 20 <label htmlFor="no"> 21 <input type="radio" id="no" name="coming" value="no" 22 23 24 /> NO 25 </label> 26 </fieldset>) 27 } 28 import useInvite from './hooks/useInvite' export default function Home () { const { inviteResponse, error, updating, updateRsvp } = useInvite() if (error) { return <div>Duh! {error}</div> } if (!inviteResponse) { return <div>Loading...</div> } function onRsvpChange (e: ChangeEvent<HTMLInputElement>) { const coming = e.target.value === 'yes' updateRsvp(coming) } return (<fieldset disabled={updating}><legend>Are you coming?</legend> <label htmlFor="yes"> <input type="radio" id="yes" name="coming" value="yes" onChange={onRsvpChange} checked={inviteResponse.invite.coming === true} /> YES </label> <label htmlFor="no"> <input type="radio" id="no" name="coming" value="no" onChange={onRsvpChange} checked={inviteResponse.invite.coming === false} /> NO </label> </fieldset>) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 loige USING THE HOOK 42
  • 43. 15 SECONDS DEMO ⏱ loige 43
  • 47. SECURITY CONSIDERATIONS loige What if I don't have an invite code and I want to hack into the website anyway? 47
  • 48. DISCLOSING SENSITIVE INFORMATION IN THE SOURCE CODE loige 48
  • 49. HOW DO WE FIX THIS? Don't hardcode any sensitive info in your JSX (or JS in general) Use the invite API to return any sensitive info (together with the user data) This way, the sensitive data is available only in the backend code loige 49
  • 50. AIRTABLE FILTER FORMULA INJECTION loige export function getInvite (inviteCode: string): Promise<Invite> { return new Promise((resolve, reject) => { base('invites') .select({ filterByFormula: `{invite} = ${escape(inviteCode)}`, maxRecords: 1 }) .firstPage((err, records) => { // ... }) }) } 1 2 3 4 5 6 7 8 9 10 11 12 filterByFormula: `{invite} = ${escape(inviteCode)}`, export function getInvite (inviteCode: string): Promise<Invite> { 1 return new Promise((resolve, reject) => { 2 base('invites') 3 .select({ 4 5 maxRecords: 1 6 }) 7 .firstPage((err, records) => { 8 // ... 9 }) 10 }) 11 } 12 50
  • 51. AIRTABLE FILTER FORMULA INJECTION loige filterByFormula: `{invite} = '${inviteCode}'`, export function getInvite (inviteCode: string): Promise<Invite> { 1 return new Promise((resolve, reject) => { 2 base('invites') 3 .select({ 4 5 maxRecords: 1 6 }) 7 .firstPage((err, records) => { 8 // ... 9 }) 10 }) 11 } 12 inviteCode is user controlled! The user can change this value arbitrarily! 😈 51
  • 52. SO WHAT?! If the user inputs the following query string: loige ?code=14b25700-fe5b-45e8-a9be-4863b6239fcf We get the following filter formula {invite} = '14b25700-fe5b-45e8-a9be-4863b6239fcf' 👌52
  • 53. SO WHAT?! But, if the user inputs this other query string: 😈 loige ?code=%27%20>%3D%200%20%26%20%27 Which is basically the following unencoded query string: {invite} = '' >= 0 & '' 😰 ?code=' >= 0 & ' Now we get: which is TRUE for EVERY RECORD! 53
  • 55. HOW DO WE FIX THIS? The escape function "sanitizes" user input to try to prevent injection Unfortunately Airtable does not provide an official solution for this, so this escape function is the best I could come up with, but it might not be "good enough"! 😔 loige function escape (value: string): string { if (value === null || typeof value === 'undefined') { return 'BLANK()' } if (typeof value === 'string') { const escapedString = value .replace(/'/g, "'") .replace(/r/g, '') .replace(//g, '') .replace(/n/g, 'n') .replace(/t/g, 't') return `'${escapedString}'` } if (typeof value === 'number') { return String(value) } if (typeof value === 'boolean') { return value ? '1' : '0' } throw Error('Invalid value received') } 55
  • 56. LET'S WRAP THINGS UP... 🌯 loige 56
  • 57. LIMITATIONS Airtable API rate limiting: 5 req/sec 😰 (We actually do 2 calls when we update a record!) loige 57
  • 58. POSSIBLE ALTERNATIVES Google spreadsheet (there's an and a ) DynamoDB (with Amplify) Firebase (?) Any headless CMS (?) Supabase or Strapi (?) API package loige 58
  • 59. TAKEAWAYS This solution is a quick, easy, and cheap way to build invite-only websites. We learned about Next.js API endpoints, custom React Hooks, how to use AirTable (and its SDK), and a bunch of security related things. Don't use this solution blindly: evaluate your context and find the best tech stack! loige 59
  • 60. ALSO AVAILABLE AS AN ARTICLE With the full ! codebase on GitHub loige loige.link/microsite-article 60
  • 61. Cover photo by on Jakob Owens Unsplash fourtheorem.com GRAZIE! 🍕❤ loige.link/pizzaparty loige 61