Skip to content

Commit 14aca61

Browse files
authored
Edited migration guide for PRAW to Devvit Web
Updated the migration guide for PRAW apps to Devvit Web, improving clarity and consistency in formatting, terminology, and structure.
1 parent fb4b1f9 commit 14aca61

1 file changed

Lines changed: 64 additions & 59 deletions

File tree

versioned_docs/version-0.13/guides/migrate/public-api.md

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,37 @@
1-
# Migrating your PRAW App to Devvit Web
1+
# Migrating Your PRAW App to Devvit Web
22

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.
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 is Reddit's modern client/server architecture for applications, allowing you to build rich moderation tools and automated bots using familiar web frameworks (like Hono and Vite).
54

6-
## **1\. Creating a Devvit App**
5+
This guide shows you how to transition your Python/PRAW app to a Devvit Web app with concepts and logic structures you're already familiar with.
76

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.
7+
## Creating a Devvit app
98

10-
### **Using the Mod Tool Template**
9+
Unlike standard Python scripts, a Devvit Web app is structurally split into a front-end client and a back-end server and tied together by a configuration file. To jumpstart your migration, you can use official Devvit templates.
1110

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.
11+
### Mod tool template
1312

14-
### **The Architecture**
13+
A highly recommended starting point for migrating PRAW moderation tools is the **Mod Tool Template**. Go 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.
14+
15+
### Architecture
1516

1617
A typical Devvit Web template will generate the following file structure:
1718

1819
- **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.
1920
- **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
2021
- **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/.
2122

22-
## **2\. Python to TypeScript: Server Concepts**
23+
## Python to TypeScript: Server Concepts
2324

2425
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:
2526

2627
- **dict vs. Object/Record:** Python dictionaries serve the same structural purpose as TypeScript objects.
2728
- **pip install vs. npm install:** Instead of managing a requirements.txt file, Devvit uses a package.json file to track dependencies.
2829
- **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.
2930

30-
## **3\. Triggers (Replacing Continuous Polling)**
31+
## Triggers (replacing continuous polling)
32+
33+
In Devvit Web, triggers are configured in your devvit.json. When an event happens (like a new comment), Devvit sends a payload to the designated endpoint on your server.
3134

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.
3335
**Step 1: Configuration (devvit.json)**
3436

3537
```json
@@ -45,45 +47,48 @@ In Devvit Web, you configure Triggers in your devvit.json. When an event happens
4547

4648
```ts
4749
// Hono is a small web framework used to define HTTP routes.
48-
import { Hono } from 'hono';
50+
import { Hono } from "hono";
4951
// TriggerResponse is the expected JSON response shape for trigger endpoints.
50-
import type { TriggerResponse } from '@devvit/web/shared';
52+
import type { TriggerResponse } from "@devvit/web/shared";
5153

5254
// Create a web server app instance.
5355
const app = new Hono();
5456

5557
// Listen for the onCommentSubmit trigger endpoint configured in devvit.json.
56-
app.post('/internal/triggers/on-comment-submit', async (c) => {
58+
app.post("/internal/triggers/on-comment-submit", async (c) => {
5759
// Parse the incoming JSON body from Devvit.
5860
// The <...> part is a TypeScript type hint for what fields we expect.
59-
const input = await c.req.json<{ author?: { username?: string; name?: string } }>();
61+
const input = await c.req.json<{
62+
author?: { username?: string; name?: string };
63+
}>();
6064
// Pick a display name safely:
6165
// - ?. means "if this exists, read it"
6266
// - ?? means "if left side is null/undefined, use right side"
63-
const authorName = input.author?.username ?? input.author?.name ?? 'unknown user';
67+
const authorName =
68+
input.author?.username ?? input.author?.name ?? "unknown user";
6469
console.log(`New comment created by ${authorName}!`);
6570
// Return a standard "ok" response with HTTP 200 status.
66-
return c.json<TriggerResponse>({ status: 'ok' }, 200);
71+
return c.json<TriggerResponse>({ status: "ok" }, 200);
6772
});
6873

6974
export default app;
7075
```
7176

72-
## **4\. Adding and Removing Comments**
77+
## Adding and removing comments
7378

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.
79+
To moderate content in Devvit Web, 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.
7580

7681
```ts
7782
// Hono handles incoming HTTP requests from Devvit.
78-
import { Hono } from 'hono';
83+
import { Hono } from "hono";
7984
// reddit is the Devvit Reddit API client for moderation/content actions.
80-
import { reddit } from '@devvit/web/server';
85+
import { reddit } from "@devvit/web/server";
8186
// TriggerResponse is the response type expected by trigger handlers.
82-
import type { TriggerResponse } from '@devvit/web/shared';
87+
import type { TriggerResponse } from "@devvit/web/shared";
8388

8489
const app = new Hono();
8590

86-
app.post('/internal/triggers/on-comment-submit', async (c) => {
91+
app.post("/internal/triggers/on-comment-submit", async (c) => {
8792
// Parse request JSON and describe expected fields with a TypeScript type.
8893
const input = await c.req.json<{
8994
author?: { id?: string };
@@ -92,53 +97,53 @@ app.post('/internal/triggers/on-comment-submit', async (c) => {
9297
// Get the comment ID if it exists.
9398
const commentId = input.comment?.id;
9499
// If we cannot find the comment ID, we cannot moderate the comment.
95-
if (!commentId) return c.json<TriggerResponse>({ status: 'ignored' }, 200);
100+
if (!commentId) return c.json<TriggerResponse>({ status: "ignored" }, 200);
96101

97102
// Normalize text to lowercase so our keyword check is case-insensitive.
98-
const body = input.comment?.body?.toLowerCase() ?? '';
103+
const body = input.comment?.body?.toLowerCase() ?? "";
99104

100105
// Check if the comment matches a specific moderation rule
101-
if (body.includes('rule-breaking string')) {
106+
if (body.includes("rule-breaking string")) {
102107
// 1. Remove the comment natively
103108
await reddit.remove(commentId, true); // true = flag as spam
104109

105110
// 2. Reply to the removed comment with a removal reason
106111
await reddit.submitComment({
107112
// Reply to the removed comment itself.
108113
id: commentId,
109-
text: 'Your comment was removed automatically for violating our community guidelines.',
114+
text: "Your comment was removed automatically for violating our community guidelines.",
110115
// Run as the app account rather than a user account.
111-
runAs: 'APP',
116+
runAs: "APP",
112117
});
113118
}
114119

115-
return c.json<TriggerResponse>({ status: 'ok' }, 200);
120+
return c.json<TriggerResponse>({ status: "ok" }, 200);
116121
});
117122

118123
export default app;
119124
```
120125

121-
## **5\. Using Redis for Storage (Replacing SQLite/JSON)**
126+
## Using Redis for storage (replacing SQLite/JSON)
122127

123128
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.
124129

125130
```ts
126131
// Hono handles HTTP routes.
127-
import { Hono } from 'hono';
132+
import { Hono } from "hono";
128133
// Redis client for key-value storage.
129-
import { redis } from '@devvit/redis';
134+
import { redis } from "@devvit/redis";
130135
// Standard trigger response type.
131-
import type { TriggerResponse } from '@devvit/web/shared';
136+
import type { TriggerResponse } from "@devvit/web/shared";
132137

133138
const app = new Hono();
134139

135-
app.post('/internal/triggers/on-post-submit', async (c) => {
140+
app.post("/internal/triggers/on-post-submit", async (c) => {
136141
// Read trigger payload JSON.
137142
const input = await c.req.json<{ author?: { id?: string } }>();
138143
// Extract the submitting user's ID.
139144
const authorId = input.author?.id;
140145
// If author is missing, skip this event safely.
141-
if (!authorId) return c.json<TriggerResponse>({ status: 'ignored' }, 200);
146+
if (!authorId) return c.json<TriggerResponse>({ status: "ignored" }, 200);
142147

143148
// Build a per-user counter key, for example: post_count:t2_abc123
144149
const redisKey = `post_count:${authorId}`;
@@ -147,15 +152,16 @@ app.post('/internal/triggers/on-post-submit', async (c) => {
147152
const newCount = await redis.incrBy(redisKey, 1);
148153
console.log(`User ${authorId} has submitted ${newCount} posts.`);
149154

150-
return c.json<TriggerResponse>({ status: 'ok' }, 200);
155+
return c.json<TriggerResponse>({ status: "ok" }, 200);
151156
});
152157

153158
export default app;
154159
```
155160

156-
## **6\. Using Schedulers (Replacing cron jobs or time.sleep)**
161+
## Using schedulers (replacing cron jobs or time.sleep)
162+
163+
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.
157164

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.
159165
**Step 1: Configuration (devvit.json)**
160166

161167
```json
@@ -168,39 +174,38 @@ PRAW bots frequently rely on time.sleep() for delayed tasks. In Devvit Web, you
168174
}
169175
}
170176
}
171-
172177
```
173178

174-
**Step 2: Scheduling and Handling (src/server/index.ts)**
179+
**Step 2: Scheduling and handling (src/server/index.ts)**
175180

176181
```ts
177182
// Hono handles incoming webhook/scheduler HTTP requests.
178-
import { Hono } from 'hono';
183+
import { Hono } from "hono";
179184
// scheduler queues delayed jobs, reddit sends private messages.
180-
import { scheduler, reddit } from '@devvit/web/server';
185+
import { scheduler, reddit } from "@devvit/web/server";
181186
// Types for scheduler request/response payloads.
182-
import type { TaskRequest, TaskResponse } from '@devvit/web/server';
187+
import type { TaskRequest, TaskResponse } from "@devvit/web/server";
183188
// Type for standard trigger responses.
184-
import type { TriggerResponse } from '@devvit/web/shared';
189+
import type { TriggerResponse } from "@devvit/web/shared";
185190

186191
const app = new Hono();
187192

188193
// 1. Triggering the scheduled job (e.g., from a comment trigger)
189-
app.post('/internal/triggers/on-comment-submit', async (c) => {
194+
app.post("/internal/triggers/on-comment-submit", async (c) => {
190195
// Parse incoming trigger JSON.
191196
// This generic type describes what data shape we expect from the payload.
192197
const input = await c.req.json<{
193198
author?: { username?: string; name?: string };
194199
comment?: { body?: string };
195200
}>();
196201
// Normalize body text so command checks are case-insensitive.
197-
const body = input.comment?.body?.toLowerCase() ?? '';
202+
const body = input.comment?.body?.toLowerCase() ?? "";
198203

199-
if (body.includes('!remindme')) {
204+
if (body.includes("!remindme")) {
200205
// Use username when available, otherwise fall back to name.
201206
const username = input.author?.username ?? input.author?.name;
202207
// If we still do not have a recipient, skip this event.
203-
if (!username) return c.json<TriggerResponse>({ status: 'ignored' }, 200);
208+
if (!username) return c.json<TriggerResponse>({ status: "ignored" }, 200);
204209

205210
// Create a timestamp one hour in the future.
206211
const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000);
@@ -210,40 +215,42 @@ app.post('/internal/triggers/on-comment-submit', async (c) => {
210215
// A unique job ID (useful for debugging/canceling).
211216
id: `remind-user-${username}-${Date.now()}`,
212217
// Must match a task name declared in devvit.json.
213-
name: 'remind-user-job',
218+
name: "remind-user-job",
214219
// Custom payload delivered later to the scheduler endpoint.
215-
data: { username, message: 'Your 1-hour reminder!' },
220+
data: { username, message: "Your 1-hour reminder!" },
216221
// Time when this job should run.
217222
runAt: oneHourFromNow,
218223
});
219224
}
220-
return c.json<TriggerResponse>({ status: 'ok' }, 200);
225+
return c.json<TriggerResponse>({ status: "ok" }, 200);
221226
});
222227

223228
// 2. The endpoint that executes when the timer concludes
224-
app.post('/internal/scheduler/remind-user-job', async (c) => {
229+
app.post("/internal/scheduler/remind-user-job", async (c) => {
225230
// Parse scheduler payload JSON.
226231
// TaskRequest<{ ... }> means "TaskRequest whose data looks like this object".
227-
const req = await c.req.json<TaskRequest<{ username: string; message: string }>>();
232+
const req =
233+
await c.req.json<TaskRequest<{ username: string; message: string }>>();
228234
// Read values from req.data safely; default to empty object if data is missing.
229235
const { username, message } = req.data ?? {};
230236
// Guard clause: ensure required fields exist before continuing.
231-
if (!username || !message) return c.json<TaskResponse>({ status: 'ignored' }, 200);
237+
if (!username || !message)
238+
return c.json<TaskResponse>({ status: "ignored" }, 200);
232239

233240
// Send a Reddit private message to the user.
234241
await reddit.sendPrivateMessage({
235242
to: username,
236-
subject: 'Automated Reminder',
243+
subject: "Automated Reminder",
237244
text: message,
238245
});
239246

240-
return c.json<TaskResponse>({ status: 'ok' }, 200);
247+
return c.json<TaskResponse>({ status: "ok" }, 200);
241248
});
242249

243250
export default app;
244251
```
245252

246-
## **Summary of Concepts**
253+
## Concept Summary
247254

248255
| Concept | PRAW (Python) | Devvit Web (Hono \+ TypeScript) |
249256
| :------------------- | :-------------------------- | :-------------------------------------------------- |
@@ -252,8 +259,6 @@ export default app;
252259
| Database Storage | SQLite, JSON, external DBs | import { redis } from '@devvit/redis' |
253260
| Delayed Actions | time.sleep() | scheduler.runJob() \+ Server Endpoint |
254261

255-
### ---
256-
257262
**References**
258263

259264
1. [Mod Tools Template - GitHub](https://github.com/reddit/devvit-template-mod-tool-devvit-web)

0 commit comments

Comments
 (0)