Skip to content

Commit 794ec9a

Browse files
Added public API migration guide (#92)
Added public API migration guide
1 parent 5e5b625 commit 794ec9a

4 files changed

Lines changed: 530 additions & 14 deletions

File tree

docs/guides/migrate/public-api.md

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# Migrating your PRAW App to Devvit Web
2+
3+
If you have built Reddit bots or moderation tools using PRAW (Python Reddit API Wrapper) and the standard Reddit API, you can port them directly into Reddit using Devvit Web. Devvit Web represents Reddit's modern client/server architecture for applications, allowing you to build rich moderation tools and automated bots utilizing familiar web frameworks (like Hono and Vite).
4+
This guide shows you how to transition your Python/PRAW app to a Devvit Web app, utilizing concepts and logic structures you are already familiar with.
5+
6+
## **1\. Creating a Devvit App**
7+
8+
Unlike standard Python scripts, a Devvit Web application is structurally split into a front-end client and a back-end server, tied together by a configuration file. To jumpstart your migration, you can utilize official Reddit templates.
9+
10+
### **Using the Mod Tool Template**
11+
12+
A highly recommended starting point for migrating PRAW moderation tools is the **Mod Tool Template**. Simply navigate to [developers.reddit.com/new](http://developers.reddit.com/new), select the Mod Tool Template and follow the instructions. The project created for you provides a complete foundation with a lightweight web framework (Hono) for backend logic, Vite for web components, and TypeScript for type safety.
13+
14+
### **The Architecture**
15+
16+
A typical Devvit Web template will generate the following file structure:
17+
18+
- **devvit.json**: This is your app's configuration file (replacing the old devvit.yaml paradigm). It defines your app's name, permissions, triggers, and scheduled jobs.
19+
- **src/client/**: This directory holds your webview code (HTML/CSS/JS or React components built with Vite). For Mod Tools it's common to not use the client folder
20+
- **src/server/**: This directory contains your backend API logic. Here, a Node server framework (like Hono) processes requests, interacts with the Reddit API, and handles triggers. All server endpoints typically start with /internal/ or /api/.
21+
22+
## **2\. Python to TypeScript: Server Concepts**
23+
24+
In PRAW, you managed state in a continuous Python loop. In Devvit Web, your application acts as an API server responding to specific incoming webhook requests (handled seamlessly by Hono). Here are the key analogies:
25+
26+
- **dict vs. Object/Record:** Python dictionaries serve the same structural purpose as TypeScript objects.
27+
- **pip install vs. npm install:** Instead of managing a requirements.txt file, Devvit uses a package.json file to track dependencies.
28+
- **Continuous Polling vs. Webhooks:** Instead of polling Reddit in a while True: loop, Devvit automatically sends a POST request to your Hono server whenever an event occurs.
29+
30+
## **3\. Triggers (Replacing Continuous Polling)**
31+
32+
In Devvit Web, you configure Triggers in your devvit.json. When an event happens (like a new comment), Devvit sends a payload to the designated endpoint on your server.
33+
**Step 1: Configuration (devvit.json)**
34+
35+
```json
36+
{
37+
"name": "my-moderator-bot",
38+
"triggers": {
39+
"onCommentSubmit": "/internal/triggers/on-comment-submit"
40+
}
41+
}
42+
```
43+
44+
**Step 2: Server Logic (src/server/index.ts)**
45+
46+
```ts
47+
// Hono is a small web framework used to define HTTP routes.
48+
import { Hono } from 'hono';
49+
// TriggerResponse is the expected JSON response shape for trigger endpoints.
50+
import type { TriggerResponse } from '@devvit/web/shared';
51+
52+
// Create a web server app instance.
53+
const app = new Hono();
54+
55+
// Listen for the onCommentSubmit trigger endpoint configured in devvit.json.
56+
app.post('/internal/triggers/on-comment-submit', async (c) => {
57+
// Parse the incoming JSON body from Devvit.
58+
// The <...> part is a TypeScript type hint for what fields we expect.
59+
const input = await c.req.json<{ author?: { username?: string; name?: string } }>();
60+
// Pick a display name safely:
61+
// - ?. means "if this exists, read it"
62+
// - ?? means "if left side is null/undefined, use right side"
63+
const authorName = input.author?.username ?? input.author?.name ?? 'unknown user';
64+
console.log(`New comment created by ${authorName}!`);
65+
// Return a standard "ok" response with HTTP 200 status.
66+
return c.json<TriggerResponse>({ status: 'ok' }, 200);
67+
});
68+
69+
export default app;
70+
```
71+
72+
## **4\. Adding and Removing Comments**
73+
74+
To moderate content in Devvit Web, you use the Reddit API client accessible within your server logic. This behaves similarly to comment.mod.remove() in PRAW but relies on asynchronous function calls.
75+
76+
```ts
77+
// Hono handles incoming HTTP requests from Devvit.
78+
import { Hono } from 'hono';
79+
// reddit is the Devvit Reddit API client for moderation/content actions.
80+
import { reddit } from '@devvit/web/server';
81+
// TriggerResponse is the response type expected by trigger handlers.
82+
import type { TriggerResponse } from '@devvit/web/shared';
83+
84+
const app = new Hono();
85+
86+
app.post('/internal/triggers/on-comment-submit', async (c) => {
87+
// Parse request JSON and describe expected fields with a TypeScript type.
88+
const input = await c.req.json<{
89+
author?: { id?: string };
90+
comment?: { id?: string; body?: string };
91+
}>();
92+
// Get the comment ID if it exists.
93+
const commentId = input.comment?.id;
94+
// If we cannot find the comment ID, we cannot moderate the comment.
95+
if (!commentId) return c.json<TriggerResponse>({ status: 'ignored' }, 200);
96+
97+
// Normalize text to lowercase so our keyword check is case-insensitive.
98+
const body = input.comment?.body?.toLowerCase() ?? '';
99+
100+
// Check if the comment matches a specific moderation rule
101+
if (body.includes('rule-breaking string')) {
102+
// 1. Remove the comment natively
103+
await reddit.remove(commentId, true); // true = flag as spam
104+
105+
// 2. Reply to the removed comment with a removal reason
106+
await reddit.submitComment({
107+
// Reply to the removed comment itself.
108+
id: commentId,
109+
text: 'Your comment was removed automatically for violating our community guidelines.',
110+
// Run as the app account rather than a user account.
111+
runAs: 'APP',
112+
});
113+
}
114+
115+
return c.json<TriggerResponse>({ status: 'ok' }, 200);
116+
});
117+
118+
export default app;
119+
```
120+
121+
## **5\. Using Redis for Storage (Replacing SQLite/JSON)**
122+
123+
Instead of maintaining a local SQLite database for tracking user warnings or config states, Devvit Web gives you direct access to a managed Redis instance.
124+
125+
```ts
126+
// Hono handles HTTP routes.
127+
import { Hono } from 'hono';
128+
// Redis client for key-value storage.
129+
import { redis } from '@devvit/redis';
130+
// Standard trigger response type.
131+
import type { TriggerResponse } from '@devvit/web/shared';
132+
133+
const app = new Hono();
134+
135+
app.post('/internal/triggers/on-post-submit', async (c) => {
136+
// Read trigger payload JSON.
137+
const input = await c.req.json<{ author?: { id?: string } }>();
138+
// Extract the submitting user's ID.
139+
const authorId = input.author?.id;
140+
// If author is missing, skip this event safely.
141+
if (!authorId) return c.json<TriggerResponse>({ status: 'ignored' }, 200);
142+
143+
// Build a per-user counter key, for example: post_count:t2_abc123
144+
const redisKey = `post_count:${authorId}`;
145+
146+
// Increment the count in Redis
147+
const newCount = await redis.incrBy(redisKey, 1);
148+
console.log(`User ${authorId} has submitted ${newCount} posts.`);
149+
150+
return c.json<TriggerResponse>({ status: 'ok' }, 200);
151+
});
152+
153+
export default app;
154+
```
155+
156+
## **6\. Using Schedulers (Replacing cron jobs or time.sleep)**
157+
158+
PRAW bots frequently rely on time.sleep() for delayed tasks. In Devvit Web, you define Scheduled Tasks in devvit.json and map them to internal Hono endpoints. You can schedule recurring jobs (like cron) or one-off tasks.
159+
**Step 1: Configuration (devvit.json)**
160+
161+
```json
162+
{
163+
"scheduler": {
164+
"tasks": {
165+
"remind-user-job": {
166+
"endpoint": "/internal/scheduler/remind-user-job"
167+
}
168+
}
169+
}
170+
}
171+
172+
```
173+
174+
**Step 2: Scheduling and Handling (src/server/index.ts)**
175+
176+
```ts
177+
// Hono handles incoming webhook/scheduler HTTP requests.
178+
import { Hono } from 'hono';
179+
// scheduler queues delayed jobs, reddit sends private messages.
180+
import { scheduler, reddit } from '@devvit/web/server';
181+
// Types for scheduler request/response payloads.
182+
import type { TaskRequest, TaskResponse } from '@devvit/web/server';
183+
// Type for standard trigger responses.
184+
import type { TriggerResponse } from '@devvit/web/shared';
185+
186+
const app = new Hono();
187+
188+
// 1. Triggering the scheduled job (e.g., from a comment trigger)
189+
app.post('/internal/triggers/on-comment-submit', async (c) => {
190+
// Parse incoming trigger JSON.
191+
// This generic type describes what data shape we expect from the payload.
192+
const input = await c.req.json<{
193+
author?: { username?: string; name?: string };
194+
comment?: { body?: string };
195+
}>();
196+
// Normalize body text so command checks are case-insensitive.
197+
const body = input.comment?.body?.toLowerCase() ?? '';
198+
199+
if (body.includes('!remindme')) {
200+
// Use username when available, otherwise fall back to name.
201+
const username = input.author?.username ?? input.author?.name;
202+
// If we still do not have a recipient, skip this event.
203+
if (!username) return c.json<TriggerResponse>({ status: 'ignored' }, 200);
204+
205+
// Create a timestamp one hour in the future.
206+
const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000);
207+
208+
// Enqueue the job
209+
await scheduler.runJob({
210+
// A unique job ID (useful for debugging/canceling).
211+
id: `remind-user-${username}-${Date.now()}`,
212+
// Must match a task name declared in devvit.json.
213+
name: 'remind-user-job',
214+
// Custom payload delivered later to the scheduler endpoint.
215+
data: { username, message: 'Your 1-hour reminder!' },
216+
// Time when this job should run.
217+
runAt: oneHourFromNow,
218+
});
219+
}
220+
return c.json<TriggerResponse>({ status: 'ok' }, 200);
221+
});
222+
223+
// 2. The endpoint that executes when the timer concludes
224+
app.post('/internal/scheduler/remind-user-job', async (c) => {
225+
// Parse scheduler payload JSON.
226+
// TaskRequest<{ ... }> means "TaskRequest whose data looks like this object".
227+
const req = await c.req.json<TaskRequest<{ username: string; message: string }>>();
228+
// Read values from req.data safely; default to empty object if data is missing.
229+
const { username, message } = req.data ?? {};
230+
// Guard clause: ensure required fields exist before continuing.
231+
if (!username || !message) return c.json<TaskResponse>({ status: 'ignored' }, 200);
232+
233+
// Send a Reddit private message to the user.
234+
await reddit.sendPrivateMessage({
235+
to: username,
236+
subject: 'Automated Reminder',
237+
text: message,
238+
});
239+
240+
return c.json<TaskResponse>({ status: 'ok' }, 200);
241+
});
242+
243+
export default app;
244+
```
245+
246+
## **Summary of Concepts**
247+
248+
| Concept | PRAW (Python) | Devvit Web (Hono \+ TypeScript) |
249+
| :------------------- | :-------------------------- | :-------------------------------------------------- |
250+
| Architecture | Continuous Running Script | Client/Server API driven by devvit.json |
251+
| Listening for Events | subreddit.stream.comments() | Webhooks handled via app.post('/internal/...', ...) |
252+
| Database Storage | SQLite, JSON, external DBs | import { redis } from '@devvit/redis' |
253+
| Delayed Actions | time.sleep() | scheduler.runJob() \+ Server Endpoint |
254+
255+
### ---
256+
257+
**References**
258+
259+
1. [Mod Tools Template - GitHub](https://github.com/reddit/devvit-template-mod-tool-devvit-web)
260+
2. [Redis](../../capabilities/server/redis.mdx)
261+
3. [Scheduler](../../capabilities/server/scheduler.mdx)
262+
4. [Triggers](../../capabilities/server/triggers.mdx)

sidebars.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -297,13 +297,7 @@ const sidebars: SidebarsConfig = {
297297
"guides/migrate/devvit-singleton",
298298
"guides/migrate/devvit-web-experimental",
299299
"guides/migrate/inline-web-view",
300-
{
301-
type: "category",
302-
label: "Splash Screens",
303-
items: [
304-
"capabilities/server/launch_screen_and_entry_points/splash_migration",
305-
],
306-
},
300+
"guides/migrate/public-api",
307301
],
308302
},
309303
{

0 commit comments

Comments
 (0)