SlideShare ist ein Scribd-Unternehmen logo
1 von 61
Downloaden Sie, um offline zu lesen
1
META_SLIDE!
loige
loige.link/micro42
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 ïŹ‚ow
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 speciïŹc 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" ïŹeld in Airtable
Add a new backend utility to update the "coming" ïŹeld for a given
invite
Add a new endpoint to update the coming ïŹeld for the current user
Update the React hook the expose the update functionality
loige 36
loige
New ïŹeld
("yes", "no", or undeïŹned)
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 ïŹlter 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 ofïŹcial 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 ïŹnd the
best tech stack!
loige 59
ALSO AVAILABLE AS AN ARTICLE
With the full !
codebase on GitHub
loige
loige.link/microsite-article
60
Photo by on
Grant Durr Unsplash
fourtheorem.com
THANKS! 🙌
loige.link/micro42
loige 61

Weitere Àhnliche Inhalte

Ähnlich wie Let's build a 0-cost invite-only website with Next.js and Airtable!

Complex Sites with Silex
Complex Sites with SilexComplex Sites with Silex
Complex Sites with SilexChris Tankersley
 
Reusing your frontend JS on the server with V8/Rhino
Reusing your frontend JS on the server with V8/RhinoReusing your frontend JS on the server with V8/Rhino
Reusing your frontend JS on the server with V8/RhinoKenneth Kalmer
 
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
 
NodeJS "Web en tiempo real"
NodeJS "Web en tiempo real"NodeJS "Web en tiempo real"
NodeJS "Web en tiempo real"SebastiĂĄn Gamboa
 
DevOpsCon 2021: Go Web Development 101
DevOpsCon 2021: Go Web Development 101DevOpsCon 2021: Go Web Development 101
DevOpsCon 2021: Go Web Development 101Jan Stamer
 
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
 
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
 
More than syntax
More than syntaxMore than syntax
More than syntaxWooga
 
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
 
Davide Cerbo - Kotlin: forse Ăš la volta buona - Codemotion Milan 2017
Davide Cerbo - Kotlin: forse Ăš la volta buona - Codemotion Milan 2017 Davide Cerbo - Kotlin: forse Ăš la volta buona - Codemotion Milan 2017
Davide Cerbo - Kotlin: forse Ăš la volta buona - Codemotion Milan 2017 Codemotion
 
Webauthn Tutorial
Webauthn TutorialWebauthn Tutorial
Webauthn TutorialFIDO Alliance
 
こわくăȘă„ă‚ˆâ€ïž Playframeworkă‚œăƒŒă‚čă‚łăƒŒăƒ‰ăƒȘăƒŒăƒ‡ă‚Łăƒłă‚°ć…„é–€
こわくăȘă„ă‚ˆâ€ïž Playframeworkă‚œăƒŒă‚čă‚łăƒŒăƒ‰ăƒȘăƒŒăƒ‡ă‚Łăƒłă‚°ć…„é–€ă“ă‚ăăȘă„ă‚ˆâ€ïž Playframeworkă‚œăƒŒă‚čă‚łăƒŒăƒ‰ăƒȘăƒŒăƒ‡ă‚Łăƒłă‚°ć…„é–€
こわくăȘă„ă‚ˆâ€ïž Playframeworkă‚œăƒŒă‚čă‚łăƒŒăƒ‰ăƒȘăƒŒăƒ‡ă‚Łăƒłă‚°ć…„é–€tanacasino
 
How and why i roll my own node.js framework
How and why i roll my own node.js frameworkHow and why i roll my own node.js framework
How and why i roll my own node.js frameworkBen Lin
 
Refactoring to Macros with Clojure
Refactoring to Macros with ClojureRefactoring to Macros with Clojure
Refactoring to Macros with ClojureDmitry Buzdin
 
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
 

Ähnlich wie Let's build a 0-cost invite-only website with Next.js and Airtable! (20)

Introduction to Groovy
Introduction to GroovyIntroduction to Groovy
Introduction to Groovy
 
Complex Sites with Silex
Complex Sites with SilexComplex Sites with Silex
Complex Sites with Silex
 
Reusing your frontend JS on the server with V8/Rhino
Reusing your frontend JS on the server with V8/RhinoReusing your frontend JS on the server with V8/Rhino
Reusing your frontend JS on the server with V8/Rhino
 
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
 
NodeJS "Web en tiempo real"
NodeJS "Web en tiempo real"NodeJS "Web en tiempo real"
NodeJS "Web en tiempo real"
 
DevOpsCon 2021: Go Web Development 101
DevOpsCon 2021: Go Web Development 101DevOpsCon 2021: Go Web Development 101
DevOpsCon 2021: Go Web Development 101
 
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
 
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
 
dotCloud and go
dotCloud and godotCloud and go
dotCloud and go
 
More than syntax
More than syntaxMore than syntax
More than syntax
 
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
 
Davide Cerbo - Kotlin: forse Ăš la volta buona - Codemotion Milan 2017
Davide Cerbo - Kotlin: forse Ăš la volta buona - Codemotion Milan 2017 Davide Cerbo - Kotlin: forse Ăš la volta buona - Codemotion Milan 2017
Davide Cerbo - Kotlin: forse Ăš la volta buona - Codemotion Milan 2017
 
Fatc
FatcFatc
Fatc
 
Webauthn Tutorial
Webauthn TutorialWebauthn Tutorial
Webauthn Tutorial
 
こわくăȘă„ă‚ˆâ€ïž Playframeworkă‚œăƒŒă‚čă‚łăƒŒăƒ‰ăƒȘăƒŒăƒ‡ă‚Łăƒłă‚°ć…„é–€
こわくăȘă„ă‚ˆâ€ïž Playframeworkă‚œăƒŒă‚čă‚łăƒŒăƒ‰ăƒȘăƒŒăƒ‡ă‚Łăƒłă‚°ć…„é–€ă“ă‚ăăȘă„ă‚ˆâ€ïž Playframeworkă‚œăƒŒă‚čă‚łăƒŒăƒ‰ăƒȘăƒŒăƒ‡ă‚Łăƒłă‚°ć…„é–€
こわくăȘă„ă‚ˆâ€ïž Playframeworkă‚œăƒŒă‚čă‚łăƒŒăƒ‰ăƒȘăƒŒăƒ‡ă‚Łăƒłă‚°ć…„é–€
 
How and why i roll my own node.js framework
How and why i roll my own node.js frameworkHow and why i roll my own node.js framework
How and why i roll my own node.js framework
 
Zone IDA Proc
Zone IDA ProcZone IDA Proc
Zone IDA Proc
 
Refactoring to Macros with Clojure
Refactoring to Macros with ClojureRefactoring to Macros with Clojure
Refactoring to Macros with Clojure
 
Intro to Ember.JS 2016
Intro to Ember.JS 2016Intro to Ember.JS 2016
Intro to Ember.JS 2016
 
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
 

Mehr von 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
 
Monoliths to the cloud!
Monoliths to the cloud!Monoliths to the cloud!
Monoliths to the cloud!Luciano 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
 
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
 
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
 

Mehr von 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
 
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
 
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
 

KĂŒrzlich hochgeladen

SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024Lorenzo Miniero
 
The Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsThe Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsPixlogix Infotech
 
"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr Bagan"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr BaganFwdays
 
DevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenDevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenHervé Boutemy
 
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptxUse of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptxLoriGlavin3
 
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptxMerck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptxLoriGlavin3
 
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptxThe Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptxLoriGlavin3
 
Artificial intelligence in cctv survelliance.pptx
Artificial intelligence in cctv survelliance.pptxArtificial intelligence in cctv survelliance.pptx
Artificial intelligence in cctv survelliance.pptxhariprasad279825
 
Dev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebDev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebUiPathCommunity
 
From Family Reminiscence to Scholarly Archive .
From Family Reminiscence to Scholarly Archive .From Family Reminiscence to Scholarly Archive .
From Family Reminiscence to Scholarly Archive .Alan Dix
 
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek SchlawackFwdays
 
How to write a Business Continuity Plan
How to write a Business Continuity PlanHow to write a Business Continuity Plan
How to write a Business Continuity PlanDatabarracks
 
Training state-of-the-art general text embedding
Training state-of-the-art general text embeddingTraining state-of-the-art general text embedding
Training state-of-the-art general text embeddingZilliz
 
Rise of the Machines: Known As Drones...
Rise of the Machines: Known As Drones...Rise of the Machines: Known As Drones...
Rise of the Machines: Known As Drones...Rick Flair
 
WordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your BrandWordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your Brandgvaughan
 
A Deep Dive on Passkeys: FIDO Paris Seminar.pptx
A Deep Dive on Passkeys: FIDO Paris Seminar.pptxA Deep Dive on Passkeys: FIDO Paris Seminar.pptx
A Deep Dive on Passkeys: FIDO Paris Seminar.pptxLoriGlavin3
 
Moving Beyond Passwords: FIDO Paris Seminar.pdf
Moving Beyond Passwords: FIDO Paris Seminar.pdfMoving Beyond Passwords: FIDO Paris Seminar.pdf
Moving Beyond Passwords: FIDO Paris Seminar.pdfLoriGlavin3
 
A Journey Into the Emotions of Software Developers
A Journey Into the Emotions of Software DevelopersA Journey Into the Emotions of Software Developers
A Journey Into the Emotions of Software DevelopersNicole Novielli
 
The State of Passkeys with FIDO Alliance.pptx
The State of Passkeys with FIDO Alliance.pptxThe State of Passkeys with FIDO Alliance.pptx
The State of Passkeys with FIDO Alliance.pptxLoriGlavin3
 
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024BookNet Canada
 

KĂŒrzlich hochgeladen (20)

SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024
 
The Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsThe Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and Cons
 
"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr Bagan"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr Bagan
 
DevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenDevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache Maven
 
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptxUse of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
 
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptxMerck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptx
 
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptxThe Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
 
Artificial intelligence in cctv survelliance.pptx
Artificial intelligence in cctv survelliance.pptxArtificial intelligence in cctv survelliance.pptx
Artificial intelligence in cctv survelliance.pptx
 
Dev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebDev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio Web
 
From Family Reminiscence to Scholarly Archive .
From Family Reminiscence to Scholarly Archive .From Family Reminiscence to Scholarly Archive .
From Family Reminiscence to Scholarly Archive .
 
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
 
How to write a Business Continuity Plan
How to write a Business Continuity PlanHow to write a Business Continuity Plan
How to write a Business Continuity Plan
 
Training state-of-the-art general text embedding
Training state-of-the-art general text embeddingTraining state-of-the-art general text embedding
Training state-of-the-art general text embedding
 
Rise of the Machines: Known As Drones...
Rise of the Machines: Known As Drones...Rise of the Machines: Known As Drones...
Rise of the Machines: Known As Drones...
 
WordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your BrandWordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your Brand
 
A Deep Dive on Passkeys: FIDO Paris Seminar.pptx
A Deep Dive on Passkeys: FIDO Paris Seminar.pptxA Deep Dive on Passkeys: FIDO Paris Seminar.pptx
A Deep Dive on Passkeys: FIDO Paris Seminar.pptx
 
Moving Beyond Passwords: FIDO Paris Seminar.pdf
Moving Beyond Passwords: FIDO Paris Seminar.pdfMoving Beyond Passwords: FIDO Paris Seminar.pdf
Moving Beyond Passwords: FIDO Paris Seminar.pdf
 
A Journey Into the Emotions of Software Developers
A Journey Into the Emotions of Software DevelopersA Journey Into the Emotions of Software Developers
A Journey Into the Emotions of Software Developers
 
The State of Passkeys with FIDO Alliance.pptx
The State of Passkeys with FIDO Alliance.pptxThe State of Passkeys with FIDO Alliance.pptx
The State of Passkeys with FIDO Alliance.pptx
 
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
 

Let's build a 0-cost invite-only website with Next.js and Airtable!

  • 1. 1
  • 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 ïŹ‚ow 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 speciïŹc 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" ïŹeld in Airtable Add a new backend utility to update the "coming" ïŹeld for a given invite Add a new endpoint to update the coming ïŹeld for the current user Update the React hook the expose the update functionality loige 36
  • 37. loige New ïŹeld ("yes", "no", or undeïŹned) 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 ïŹlter 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 ofïŹcial 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 ïŹnd 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. Photo by on Grant Durr Unsplash fourtheorem.com THANKS! 🙌 loige.link/micro42 loige 61