Skip to content

Commit 3932b03

Browse files
committed
add strong types for all endpoints
1 parent bb5d3e5 commit 3932b03

20 files changed

Lines changed: 1450 additions & 878 deletions

docs/capabilities/client/forms.mdx

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,18 @@ For forms that open from a menu item, you can use menu responses. This is useful
182182
<TabItem value="hono">
183183

184184
```ts title="server/index.ts"
185+
import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
186+
187+
type NameFormRequest = { name: string };
188+
type ReviewFormRequest = { review: string };
189+
185190
// Menu action that triggers menu response form
186191
app.post('/internal/menu/start-workflow', async (c) => {
192+
const _input = await c.req.json<MenuItemRequest>();
187193
// Server processing before showing form
188194
const userData = await fetchUserData();
189195

190-
return c.json({
196+
return c.json<UiResponse>({
191197
showForm: {
192198
name: 'nameForm',
193199
form: {
@@ -206,13 +212,13 @@ For forms that open from a menu item, you can use menu responses. This is useful
206212

207213
// Form submission handler that can chain to another form
208214
app.post('/internal/form/name-submit', async (c) => {
209-
const { name } = await c.req.json();
215+
const { name } = await c.req.json<NameFormRequest>();
210216

211217
// Server processing
212218
await saveUserName(name);
213219

214220
// Show next form in workflow
215-
return c.json({
221+
return c.json<UiResponse>({
216222
showForm: {
217223
name: 'reviewForm',
218224
form: {
@@ -229,11 +235,11 @@ For forms that open from a menu item, you can use menu responses. This is useful
229235
});
230236

231237
app.post('/internal/form/review-submit', async (c) => {
232-
const { review } = await c.req.json();
238+
const { review } = await c.req.json<ReviewFormRequest>();
233239

234240
await saveReview(review);
235241

236-
return c.json({
242+
return c.json<UiResponse>({
237243
showToast: 'Thank you for your feedback!'
238244
});
239245
});
@@ -243,10 +249,13 @@ For forms that open from a menu item, you can use menu responses. This is useful
243249
<TabItem value="express">
244250

245251
```ts title="server/index.ts"
246-
import { UIResponse } from '@devvit/web/shared';
252+
import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
253+
254+
type NameFormRequest = { name: string };
255+
type ReviewFormRequest = { review: string };
247256

248257
// Menu action that triggers menu response form
249-
router.post("/internal/menu/start-workflow", async (_req, res: Response<UIResponse>) => {
258+
router.post<string, never, UiResponse, MenuItemRequest>("/internal/menu/start-workflow", async (_req, res) => {
250259
// Server processing before showing form
251260
const userData = await fetchUserData();
252261

@@ -268,7 +277,7 @@ For forms that open from a menu item, you can use menu responses. This is useful
268277
});
269278

270279
// Form submission handler that can chain to another form
271-
router.post("/internal/form/name-submit", async (req, res: Response<UIResponse>) => {
280+
router.post<string, never, UiResponse, NameFormRequest>("/internal/form/name-submit", async (req, res) => {
272281
const { name } = req.body;
273282

274283
// Server processing
@@ -291,7 +300,7 @@ For forms that open from a menu item, you can use menu responses. This is useful
291300
});
292301
});
293302

294-
router.post("/internal/form/review-submit", async (req, res: Response<UIResponse>) => {
303+
router.post<string, never, UiResponse, ReviewFormRequest>("/internal/form/review-submit", async (req, res) => {
295304
const { review } = req.body;
296305

297306
await saveReview(review);
@@ -677,11 +686,16 @@ Below is a collection of common use cases and patterns.
677686
<TabItem value="hono">
678687

679688
```ts title="server/index.ts"
689+
import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
690+
691+
type DynamicFormRequest = { username: string };
692+
680693
// Endpoint that shows form with dynamic data
681694
app.post('/internal/menu/show-dynamic-form', async (c) => {
695+
const _input = await c.req.json<MenuItemRequest>();
682696
const user = await reddit.getCurrentUser();
683697

684-
return c.json({
698+
return c.json<UiResponse>({
685699
showForm: {
686700
name: 'dynamicForm',
687701
form: {
@@ -702,9 +716,9 @@ Below is a collection of common use cases and patterns.
702716

703717
// Form submission handler
704718
app.post('/internal/form/dynamic-submit', async (c) => {
705-
const { username } = await c.req.json();
719+
const { username } = await c.req.json<DynamicFormRequest>();
706720

707-
return c.json({
721+
return c.json<UiResponse>({
708722
showToast: `Hello ${username}`
709723
});
710724
});
@@ -714,8 +728,12 @@ Below is a collection of common use cases and patterns.
714728
<TabItem value="express">
715729

716730
```ts title="server/index.ts"
731+
import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
732+
733+
type DynamicFormRequest = { username: string };
734+
717735
// Endpoint that shows form with dynamic data
718-
router.post("/internal/menu/show-dynamic-form", async (_req, res: Response<UiResponse>) => {
736+
router.post<string, never, UiResponse, MenuItemRequest>("/internal/menu/show-dynamic-form", async (_req, res) => {
719737
const user = await reddit.getCurrentUser();
720738

721739
res.json({
@@ -738,7 +756,7 @@ Below is a collection of common use cases and patterns.
738756
});
739757

740758
// Form submission handler
741-
router.post("/internal/form/dynamic-submit", async (req, res: Response<UiResponse>) => {
759+
router.post<string, never, UiResponse, DynamicFormRequest>("/internal/form/dynamic-submit", async (req, res) => {
742760
const { username } = req.body;
743761

744762
res.json({
@@ -889,11 +907,17 @@ Below is a collection of common use cases and patterns.
889907
<TabItem value="hono">
890908

891909
```ts title="server/index.ts"
910+
import type { UiResponse } from '@devvit/web/shared';
911+
912+
type Step1FormRequest = { name: string };
913+
type Step2FormRequest = { name: string; food: string };
914+
type Step3FormRequest = { name: string; food: string; drink: string };
915+
892916
// Step 1: Name form
893917
app.post('/internal/form/step1-submit', async (c) => {
894-
const { name } = await c.req.json();
918+
const { name } = await c.req.json<Step1FormRequest>();
895919

896-
return c.json({
920+
return c.json<UiResponse>({
897921
showForm: {
898922
name: 'step2Form',
899923
form: {
@@ -913,9 +937,9 @@ Below is a collection of common use cases and patterns.
913937

914938
// Step 2: Food form
915939
app.post('/internal/form/step2-submit', async (c) => {
916-
const { name, food } = await c.req.json();
940+
const { name, food } = await c.req.json<Step2FormRequest>();
917941

918-
return c.json({
942+
return c.json<UiResponse>({
919943
showForm: {
920944
name: 'step3Form',
921945
form: {
@@ -935,9 +959,9 @@ Below is a collection of common use cases and patterns.
935959

936960
// Step 3: Final form
937961
app.post('/internal/form/step3-submit', async (c) => {
938-
const { name, food, drink } = await c.req.json();
962+
const { name, food, drink } = await c.req.json<Step3FormRequest>();
939963

940-
return c.json({
964+
return c.json<UiResponse>({
941965
showToast: `Thanks ${name}! You like ${food} and ${drink}.`
942966
});
943967
});
@@ -947,8 +971,14 @@ Below is a collection of common use cases and patterns.
947971
<TabItem value="express">
948972

949973
```ts title="server/index.ts"
974+
import type { UiResponse } from '@devvit/web/shared';
975+
976+
type Step1FormRequest = { name: string };
977+
type Step2FormRequest = { name: string; food: string };
978+
type Step3FormRequest = { name: string; food: string; drink: string };
979+
950980
// Step 1: Name form
951-
router.post("/internal/form/step1-submit", async (req, res: Response<UiResponse>) => {
981+
router.post<string, never, UiResponse, Step1FormRequest>("/internal/form/step1-submit", async (req, res) => {
952982
const { name } = req.body;
953983

954984
res.json({
@@ -970,7 +1000,7 @@ Below is a collection of common use cases and patterns.
9701000
});
9711001

9721002
// Step 2: Food form
973-
router.post("/internal/form/step2-submit", async (req, res: Response<UiResponse>) => {
1003+
router.post<string, never, UiResponse, Step2FormRequest>("/internal/form/step2-submit", async (req, res) => {
9741004
const { name, food } = req.body;
9751005

9761006
res.json({
@@ -992,7 +1022,7 @@ Below is a collection of common use cases and patterns.
9921022
});
9931023

9941024
// Step 3: Final form
995-
router.post("/internal/form/step3-submit", async (req, res: Response<UiResponse>) => {
1025+
router.post<string, never, UiResponse, Step3FormRequest>("/internal/form/step3-submit", async (req, res) => {
9961026
const { name, food, drink } = req.body;
9971027

9981028
res.json({
@@ -1186,18 +1216,29 @@ This example includes one of each of the [supported field types](#supported-fiel
11861216
<TabItem value="hono">
11871217

11881218
```ts title="server/index.ts"
1219+
import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
1220+
1221+
type EverythingFormRequest = {
1222+
food: string;
1223+
times?: number;
1224+
what?: string;
1225+
healthy?: string[];
1226+
again?: boolean;
1227+
};
1228+
11891229
app.post('/internal/form/everything-submit', async (c) => {
1190-
const formValues = await c.req.json();
1230+
const formValues = await c.req.json<EverythingFormRequest>();
11911231
console.log('Form values:', formValues);
11921232

1193-
return c.json({
1233+
return c.json<UiResponse>({
11941234
showToast: 'Thanks!'
11951235
});
11961236
});
11971237

11981238
// Example showing the form
11991239
app.post('/internal/menu/show-everything-form', async (c) => {
1200-
return c.json({
1240+
const _input = await c.req.json<MenuItemRequest>();
1241+
return c.json<UiResponse>({
12011242
showForm: {
12021243
name: 'everythingForm',
12031244
form: {
@@ -1257,7 +1298,17 @@ This example includes one of each of the [supported field types](#supported-fiel
12571298
<TabItem value="express">
12581299

12591300
```ts title="server/index.ts"
1260-
router.post("/internal/form/everything-submit", async (req, res: Response<UiResponse>) => {
1301+
import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
1302+
1303+
type EverythingFormRequest = {
1304+
food: string;
1305+
times?: number;
1306+
what?: string;
1307+
healthy?: string[];
1308+
again?: boolean;
1309+
};
1310+
1311+
router.post<string, never, UiResponse, EverythingFormRequest>("/internal/form/everything-submit", async (req, res) => {
12611312
console.log('Form values:', req.body);
12621313

12631314
res.json({
@@ -1266,7 +1317,7 @@ This example includes one of each of the [supported field types](#supported-fiel
12661317
});
12671318

12681319
// Example showing the form
1269-
router.post("/internal/menu/show-everything-form", async (_req, res: Response<UiResponse>) => {
1320+
router.post<string, never, UiResponse, MenuItemRequest>("/internal/menu/show-everything-form", async (_req, res) => {
12701321
res.json({
12711322
showForm: {
12721323
name: 'everythingForm',
@@ -1452,12 +1503,16 @@ This example includes one of each of the [supported field types](#supported-fiel
14521503
<TabItem value="hono">
14531504

14541505
```ts title="server/index.ts"
1506+
import type { UiResponse } from '@devvit/web/shared';
1507+
1508+
type ImageFormRequest = { myImage: string };
1509+
14551510
app.post('/internal/form/image-submit', async (c) => {
1456-
const { myImage } = await c.req.json();
1511+
const { myImage } = await c.req.json<ImageFormRequest>();
14571512
// Use the mediaUrl to store in redis and display it in an <image> block, or send to external service to modify
14581513
console.log('Image uploaded:', myImage);
14591514

1460-
return c.json({
1515+
return c.json<UiResponse>({
14611516
showToast: 'Image uploaded successfully!'
14621517
});
14631518
});
@@ -1467,7 +1522,11 @@ This example includes one of each of the [supported field types](#supported-fiel
14671522
<TabItem value="express">
14681523

14691524
```ts title="server/index.ts"
1470-
router.post("/internal/form/image-submit", async (req, res: Response<UiResponse>) => {
1525+
import type { UiResponse } from '@devvit/web/shared';
1526+
1527+
type ImageFormRequest = { myImage: string };
1528+
1529+
router.post<string, never, UiResponse, ImageFormRequest>("/internal/form/image-submit", async (req, res) => {
14711530
const { myImage } = req.body;
14721531
// Use the mediaUrl to store in redis and display it in an <image> block, or send to external service to modify
14731532
console.log('Image uploaded:', myImage);

docs/capabilities/client/menu-actions.mdx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,12 @@ Add an item to the three dot menu for posts, comments, or subreddits. Menu actio
4343
<TabItem value="hono">
4444

4545
```ts title="server/index.ts"
46+
import type { MenuItemRequest, UiResponse } from "@devvit/web/shared";
47+
4648
app.post("/internal/menu/show-info", async (c) => {
49+
const _input = await c.req.json<MenuItemRequest>();
4750
// Simple actions don't need server processing
48-
return c.json({
51+
return c.json<UiResponse>({
4952
showToast: "Menu action clicked!",
5053
});
5154
});
@@ -55,12 +58,17 @@ app.post("/internal/menu/show-info", async (c) => {
5558
<TabItem value="express">
5659

5760
```ts title="server/index.ts"
58-
app.post("/internal/menu/show-info", async (_req, res) => {
59-
// Simple actions don't need server processing
60-
res.json({
61-
showToast: "Menu action clicked!",
62-
});
63-
});
61+
import type { MenuItemRequest, UiResponse } from "@devvit/web/shared";
62+
63+
app.post<string, never, UiResponse, MenuItemRequest>(
64+
"/internal/menu/show-info",
65+
async (_req, res) => {
66+
// Simple actions don't need server processing
67+
res.json({
68+
showToast: "Menu action clicked!",
69+
});
70+
},
71+
);
6472
```
6573

6674
</TabItem>
@@ -162,13 +170,16 @@ In Devvit Web, your menu item should respond with a client side effect to give f
162170
<TabItem value="hono">
163171

164172
```ts title="server/index.ts"
173+
import type { MenuItemRequest, UiResponse } from "@devvit/web/shared";
174+
165175
app.post("/internal/menu/complex-action", async (c) => {
176+
const _input = await c.req.json<MenuItemRequest>();
166177
try {
167178
// Perform server-side processing
168179
const userData = await validateAndProcessData();
169180

170181
// Show form with server-fetched data
171-
return c.json({
182+
return c.json<UiResponse>({
172183
showForm: {
173184
name: "processForm",
174185
form: {
@@ -184,7 +195,7 @@ app.post("/internal/menu/complex-action", async (c) => {
184195
},
185196
});
186197
} catch (error) {
187-
return c.json({
198+
return c.json<UiResponse>({
188199
showToast: "Processing failed. Please try again.",
189200
});
190201
}
@@ -195,11 +206,11 @@ app.post("/internal/menu/complex-action", async (c) => {
195206
<TabItem value="express">
196207

197208
```ts title="server/index.ts"
198-
import { UIResponse } from "@devvit/web/shared";
209+
import type { MenuItemRequest, UiResponse } from "@devvit/web/shared";
199210

200-
app.post(
211+
app.post<string, never, UiResponse, MenuItemRequest>(
201212
"/internal/menu/complex-action",
202-
async (_req, res: Response<UIResponse>) => {
213+
async (_req, res) => {
203214
try {
204215
// Perform server-side processing
205216
const userData = await validateAndProcessData();

0 commit comments

Comments
 (0)