3C. Integrate a Hello Image Job
Connect the Hello Next.js page and Flask blueprint through Redis, an RQ worker, and a generated PNG.
Integrate a Hello Image-Generation Job
Expected time: 90–150 minutes
Outcome: A developer enters text in the Next.js page, receives a job ID, watches an RQ job progress, and sees the PNG generated by the Model Server worker.
This tutorial extends the files created in 3A. Build Hello Marsad and 3B. Build Hello Model Server. Do not clean up those exercises until the final group on this page.
The image is deterministic and generated with Pillow, which is already a Model Server dependency. No external model or provider credential is required; the purpose is to learn Marsad's integration boundaries.
Browser
→ Next.js page
→ Next.js API route
→ authenticated Model Server route
→ Redis tasks queue
→ RQ worker
→ shared-data/generated/tutorial/<job-id>.png
→ Next.js image proxy
→ BrowserEach group has a visible checkpoint. Do not continue until your output matches the checkpoint.
Starting Checkpoint
Start the Web App, Model Server, Redis, and the worker that consumes tasks. Then confirm:
- http://localhost:3511/tutorial/hello-world displays the page from 3A.
- http://localhost:3531/tutorial/hello returns the response from 3B:
{
"message": "Hello, Marsad!",
"service": "model-server"
}If either output is missing, return to its tutorial before continuing.
Group 1: Connect the Two Hello Worlds
The browser will call a Next.js route. That route will call Flask from the server, keeping the Model Server address out of browser code.
Step 1. Create the Next.js proxy
Create main/src/app/api/tutorial/model-hello/route.ts:
import {NextResponse} from 'next/server';
export const runtime = 'nodejs';
function getModelServerOrigin() {
const configuredUrl = process.env.MODEL_API_URL;
if (!configuredUrl) {
throw new Error('MODEL_API_URL is not configured.');
}
return configuredUrl.replace(/\/api\/?$/, '').replace(/\/$/, '');
}
export async function GET() {
try {
const response = await fetch(`${getModelServerOrigin()}/tutorial/hello`, {
cache: 'no-store',
});
const modelServer = await response.json();
if (!response.ok) {
return NextResponse.json(
{
success: false,
message: 'The Model Server rejected the request.',
modelServer,
},
{status: response.status},
);
}
return NextResponse.json({
success: true,
web_app: 'connected',
model_server: modelServer,
});
} catch (error) {
return NextResponse.json(
{
success: false,
message:
error instanceof Error
? error.message
: 'The Model Server request failed.',
},
{status: 502},
);
}
}MODEL_API_URL normally ends in /api; the temporary 3B endpoint does not. This first proxy deliberately derives the Model Server origin. Group 4 will move the tutorial blueprint into the authenticated /api namespace.
Step 2. Call the proxy
Open http://localhost:3511/api/tutorial/model-hello.
Expected output
{
"success": true,
"web_app": "connected",
"model_server": {
"message": "Hello, Marsad!",
"service": "model-server"
}
}If you receive 502, check the Model Server terminal, port 3531, and MODEL_API_URL.
Step 3. Link the result from the Hello page
In main/src/app/(landing)/tutorial/hello-world/page.tsx, import Link:
import Link from 'next/link';Add this link below the existing supporting text:
<Link
href='/api/tutorial/model-hello'
className='rounded-md bg-primary px-4 py-2 text-primary-foreground'
>
Check Model Server
</Link>Open the Hello page and select Check Model Server.
Group 1 checkpoint
The link opens the JSON response containing both web_app: "connected" and the Model Server greeting. Explain why the browser called port 3511, not port 3531, before continuing.
Group 2: Generate a PNG Without Redis
Prove that image generation works before putting it behind a queue.
Step 4. Create the pure image task
Create model-server/src/models/hello_image_task.py:
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
IMAGE_SIZE = (1200, 630)
DEFAULT_OUTPUT_ROOT = Path("shared-data/generated/tutorial")
def generate_hello_image(
job_id: str,
text: str,
output_root: str | Path = DEFAULT_OUTPUT_ROOT,
):
output_dir = Path(output_root)
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"{job_id}.png"
image = Image.new("RGB", IMAGE_SIZE, color="#f8fafc")
draw = ImageDraw.Draw(image)
font = ImageFont.load_default(size=64)
text_box = draw.textbbox((0, 0), text, font=font)
text_width = text_box[2] - text_box[0]
text_height = text_box[3] - text_box[1]
position = (
(IMAGE_SIZE[0] - text_width) / 2,
(IMAGE_SIZE[1] - text_height) / 2,
)
draw.text(position, text, fill="#0f172a", font=font)
image.save(output_path, format="PNG")
return {
"image_path": output_path.as_posix(),
"width": IMAGE_SIZE[0],
"height": IMAGE_SIZE[1],
}The function receives a server-generated job ID and writes only below a caller-controlled output root. The HTTP request will never supply a filename or directory.
Step 5. Test the function in isolation
Create model-server/tests/test_hello_image_task.py:
from PIL import Image
from src.models.hello_image_task import generate_hello_image
def test_generate_hello_image_writes_png(tmp_path):
result = generate_hello_image(
job_id="hello-preview",
text="Hello, Marsad!",
output_root=tmp_path,
)
image_path = tmp_path / "hello-preview.png"
assert image_path.exists()
assert result["image_path"] == image_path.as_posix()
with Image.open(image_path) as image:
assert image.format == "PNG"
assert image.size == (1200, 630)From model-server/, run:
pytest tests/test_hello_image_task.pyExpected output
tests/test_hello_image_task.py . [100%]
1 passedStep 6. Create a visible preview
From model-server/, run:
python -c "from src.models.hello_image_task import generate_hello_image; print(generate_hello_image('preview', 'Hello, Marsad!'))"Expected output
The command prints a result containing:
shared-data/generated/tutorial/preview.pngOpen that file. It should be a 1200×630 image with “Hello, Marsad!” centered on it.
Group 2 checkpoint
The focused test passes and you can open the generated preview PNG. Do not continue if only the Python function succeeds but no readable image exists.
Group 3: Turn the Function Into an RQ Job
Now add Redis, the tasks queue, job status, and an image response.
Step 7. Extend the Flask blueprint
Replace model-server/src/routes/hello.py with:
from pathlib import Path
from uuid import uuid4
from flask import Blueprint, jsonify, request, send_file
from rq.exceptions import NoSuchJobError
from rq.job import Job, JobStatus
from src.database.redis_client import redis_client
from src.models.hello_image_task import generate_hello_image
from src.queues.config import task_queue
hello_bp = Blueprint("hello", __name__, url_prefix="/tutorial")
OUTPUT_ROOT = Path("shared-data/generated/tutorial")
@hello_bp.get("/hello")
def hello():
return jsonify(
{
"message": "Hello, Marsad!",
"service": "model-server",
}
)
@hello_bp.post("/hello-image")
def enqueue_hello_image():
payload = request.get_json(silent=True) or {}
text = payload.get("text")
if not isinstance(text, str) or not 1 <= len(text.strip()) <= 80:
return jsonify(
{
"success": False,
"message": "Text must contain between 1 and 80 characters.",
}
), 422
job_id = f"hello-image-{uuid4().hex}"
task_queue.enqueue(
generate_hello_image,
job_id,
text.strip(),
job_id=job_id,
job_timeout=60,
)
return jsonify(
{
"success": True,
"job_id": job_id,
"status": "queued",
}
), 202
@hello_bp.get("/hello-image/<job_id>")
def get_hello_image_job(job_id):
try:
job = Job.fetch(job_id, connection=redis_client)
except NoSuchJobError:
return jsonify(
{
"success": False,
"message": "Tutorial job not found.",
}
), 404
status_map = {
JobStatus.QUEUED: "queued",
JobStatus.STARTED: "processing",
JobStatus.FINISHED: "completed",
JobStatus.FAILED: "failed",
JobStatus.DEFERRED: "queued",
JobStatus.SCHEDULED: "queued",
JobStatus.STOPPED: "failed",
JobStatus.CANCELED: "failed",
}
status = status_map.get(job.get_status(refresh=True), "queued")
response = {
"success": status != "failed",
"job_id": job_id,
"status": status,
}
if status == "completed":
response["image_url"] = f"/tutorial/hello-image/{job_id}/image"
elif status == "failed":
response["message"] = "Image generation failed. Check the worker log."
return jsonify(response)
@hello_bp.get("/hello-image/<job_id>/image")
def get_hello_image(job_id):
try:
job = Job.fetch(job_id, connection=redis_client)
except NoSuchJobError:
return jsonify(
{
"success": False,
"message": "Tutorial job not found.",
}
), 404
if job.get_status(refresh=True) != JobStatus.FINISHED:
return jsonify(
{
"success": False,
"message": "The tutorial image is not ready.",
}
), 409
result = job.return_value(refresh=True) or {}
image_value = result.get("image_path")
if not isinstance(image_value, str):
return jsonify(
{
"success": False,
"message": "The job did not produce an image path.",
}
), 500
allowed_root = OUTPUT_ROOT.resolve()
image_path = Path(image_value).resolve()
try:
image_path.relative_to(allowed_root)
except ValueError:
return jsonify(
{
"success": False,
"message": "The generated image path is invalid.",
}
), 500
if not image_path.is_file():
return jsonify(
{
"success": False,
"message": "The generated image is missing.",
}
), 404
return send_file(image_path, mimetype="image/png")Restart the Model Server after changing the blueprint.
Step 8. Test enqueueing without a worker
Extend model-server/tests/test_hello_route.py:
from src.routes import hello as hello_routes
def test_enqueue_hello_image_returns_job_id(monkeypatch):
app = Flask(__name__)
app.register_blueprint(hello_bp)
enqueue_calls = []
monkeypatch.setattr(
hello_routes.task_queue,
"enqueue",
lambda *args, **kwargs: enqueue_calls.append((args, kwargs)),
)
with app.test_client() as client:
response = client.post(
"/tutorial/hello-image",
json={"text": "Hello from pytest!"},
)
assert response.status_code == 202
data = response.get_json()
assert data["status"] == "queued"
assert data["job_id"].startswith("hello-image-")
assert enqueue_calls
args, kwargs = enqueue_calls[0]
assert args[0] is hello_routes.generate_hello_image
assert args[1] == data["job_id"]
assert args[2] == "Hello from pytest!"
assert kwargs["job_id"] == data["job_id"]Keep the imports already present in the 3B test. Run:
pytest tests/test_hello_route.pyExpected output
tests/test_hello_route.py .. [100%]
2 passedNo worker or Redis connection is used: monkeypatch replaces the queue operation and lets the test inspect what would have been enqueued.
Step 9. Enqueue the real job
In PowerShell:
$body = @{ text = "Hello from an RQ job!" } | ConvertTo-Json
$job = Invoke-RestMethod -Method Post -Uri http://localhost:3531/tutorial/hello-image -ContentType "application/json" -Body $body
$jobOn macOS or Linux:
curl -X POST http://localhost:3531/tutorial/hello-image \
-H "Content-Type: application/json" \
-d '{"text":"Hello from an RQ job!"}'Expected output
{
"success": true,
"job_id": "hello-image-...",
"status": "queued"
}Keep the returned job ID.
Step 10. Observe the worker and file
The worker consuming tasks should report that it received and completed generate_hello_image. A matching PNG should appear beneath:
model-server/shared-data/generated/tutorial/Because model-server/shared-data points to the common data directory, the same file should be visible beneath main/shared-data/generated/tutorial/.
Step 11. Check status
In PowerShell:
Invoke-RestMethod "http://localhost:3531/tutorial/hello-image/$($job.job_id)"On macOS or Linux, replace the example job ID:
curl http://localhost:3531/tutorial/hello-image/hello-image-your-job-idExpected output after completion
{
"success": true,
"job_id": "hello-image-...",
"status": "completed",
"image_url": "/tutorial/hello-image/hello-image-.../image"
}Open the full image URL on port 3531. The generated PNG should render in the browser.
Step 12. Prove that the work is asynchronous
Stop only the worker that consumes tasks; leave Redis and the Model Server running. Enqueue a second job and check its status.
Expected output
{
"success": true,
"job_id": "hello-image-...",
"status": "queued"
}No matching PNG should exist yet. Restart the worker, check the same job ID again, and confirm that it becomes completed.
Group 3 checkpoint
One request returned 202, the worker generated the image afterward, the same job ID reported queued and then completed, and the image URL rendered the PNG.
Group 4: Protect the Model Server Boundary
The browser-facing integration must not leave the tutorial job endpoints public.
Step 13. Move the blueprint into /api
In model-server/src/routes/hello.py, change:
hello_bp = Blueprint("hello", __name__, url_prefix="/tutorial")to:
hello_bp = Blueprint("hello", __name__, url_prefix="/api/tutorial")Update the completed job response:
response["image_url"] = f"/api/tutorial/hello-image/{job_id}/image"Update both URLs in tests/test_hello_route.py from /tutorial/... to /api/tutorial/..., then run the test and confirm that both cases still pass.
Step 14. Apply bearer authentication
In model-server/app.py, ensure hello_bp remains imported and registered. Add it to the protect_blueprints list:
protect_blueprints(app, [
bluesky_bp, youtube_bp, reddit_bp,
telegram_bp, tanbih_bp, marsad_bp,
hello_bp,
], exempt_endpoints={"rq_dashboard.health"})Restart the Model Server.
Step 15. Confirm direct access is rejected
Open:
http://localhost:3531/api/tutorial/helloExpected output
401 UnauthorizedDo not paste the bearer token into a browser URL or commit it in tutorial code.
Step 16. Update the first Next.js proxy
Replace the origin helper and fetch call in main/src/app/api/tutorial/model-hello/route.ts with the configured /api base and bearer header:
function getModelServerConfig() {
const baseUrl = process.env.MODEL_API_URL?.replace(/\/$/, '');
const token = process.env.MODEL_API_TOKEN;
if (!baseUrl || !token) {
throw new Error('Model Server configuration is incomplete.');
}
return {baseUrl, token};
}Inside GET, call:
const {baseUrl, token} = getModelServerConfig();
const response = await fetch(`${baseUrl}/tutorial/hello`, {
cache: 'no-store',
headers: {
Authorization: `Bearer ${token}`,
},
});Open http://localhost:3511/api/tutorial/model-hello again.
Expected output
The Next.js URL still returns web_app: "connected" and the Model Server greeting, while the direct Model Server URL returns 401.
Group 4 checkpoint
The browser can reach the protected greeting only through Next.js. Explain where MODEL_API_TOKEN is read and why it never appears in the Client Component.
Group 5: Complete the Next.js Job Integration
Add server-side proxy routes, then evolve the 3A page into an interactive job interface.
Step 17. Add a shared Model Server helper
Create main/src/app/api/tutorial/_lib/model-server.ts:
export function getTutorialModelServerConfig() {
const baseUrl = process.env.MODEL_API_URL?.replace(/\/$/, '');
const token = process.env.MODEL_API_TOKEN;
if (!baseUrl || !token) {
throw new Error('Model Server configuration is incomplete.');
}
return {
baseUrl,
headers: {
Authorization: `Bearer ${token}`,
},
};
}Step 18. Add the enqueue proxy
Create main/src/app/api/tutorial/hello-image/route.ts:
import {NextResponse} from 'next/server';
import {getTutorialModelServerConfig} from '@/app/api/tutorial/_lib/model-server';
export const runtime = 'nodejs';
export async function POST(request: Request) {
try {
const payload = await request.json().catch(() => null);
const text =
typeof payload?.text === 'string'
? payload.text.trim()
: '';
if (!text || text.length > 80) {
return NextResponse.json(
{
success: false,
message: 'Text must contain between 1 and 80 characters.',
},
{status: 422},
);
}
const {baseUrl, headers} = getTutorialModelServerConfig();
const response = await fetch(`${baseUrl}/tutorial/hello-image`, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({text}),
cache: 'no-store',
});
return new NextResponse(await response.text(), {
status: response.status,
headers: {
'Content-Type':
response.headers.get('content-type') ??
'application/json',
},
});
} catch (error) {
return NextResponse.json(
{
success: false,
message:
error instanceof Error
? error.message
: 'Could not enqueue the tutorial job.',
},
{status: 502},
);
}
}Step 19. Add the status proxy
Create main/src/app/api/tutorial/hello-image/[jobId]/route.ts:
import {NextResponse} from 'next/server';
import {getTutorialModelServerConfig} from '@/app/api/tutorial/_lib/model-server';
export const runtime = 'nodejs';
export async function GET(
_request: Request,
context: {params: Promise<{jobId: string}>},
) {
const {jobId} = await context.params;
if (!/^hello-image-[a-f0-9]{32}$/.test(jobId)) {
return NextResponse.json(
{success: false, message: 'Invalid tutorial job ID.'},
{status: 400},
);
}
try {
const {baseUrl, headers} = getTutorialModelServerConfig();
const response = await fetch(
`${baseUrl}/tutorial/hello-image/${jobId}`,
{
headers,
cache: 'no-store',
},
);
return new NextResponse(await response.text(), {
status: response.status,
headers: {
'Content-Type':
response.headers.get('content-type') ??
'application/json',
},
});
} catch {
return NextResponse.json(
{
success: false,
message: 'Could not read the tutorial job.',
},
{status: 502},
);
}
}Step 20. Add the image proxy
Create main/src/app/api/tutorial/hello-image/[jobId]/image/route.ts:
import {NextResponse} from 'next/server';
import {getTutorialModelServerConfig} from '@/app/api/tutorial/_lib/model-server';
export const runtime = 'nodejs';
export async function GET(
_request: Request,
context: {params: Promise<{jobId: string}>},
) {
const {jobId} = await context.params;
if (!/^hello-image-[a-f0-9]{32}$/.test(jobId)) {
return NextResponse.json(
{success: false, message: 'Invalid tutorial job ID.'},
{status: 400},
);
}
try {
const {baseUrl, headers} = getTutorialModelServerConfig();
const response = await fetch(
`${baseUrl}/tutorial/hello-image/${jobId}/image`,
{
headers,
cache: 'no-store',
},
);
if (!response.ok) {
return new NextResponse(await response.text(), {
status: response.status,
headers: {
'Content-Type':
response.headers.get('content-type') ??
'application/json',
},
});
}
return new NextResponse(await response.arrayBuffer(), {
status: 200,
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'no-store',
},
});
} catch {
return NextResponse.json(
{
success: false,
message: 'Could not load the tutorial image.',
},
{status: 502},
);
}
}Step 21. Test the enqueue proxy before building UI
In PowerShell:
$body = @{ text = "Hello from Next.js!" } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://localhost:3511/api/tutorial/hello-image -ContentType "application/json" -Body $bodyOn macOS or Linux:
curl -X POST http://localhost:3511/api/tutorial/hello-image \
-H "Content-Type: application/json" \
-d '{"text":"Hello from Next.js!"}'Expected output
{
"success": true,
"job_id": "hello-image-...",
"status": "queued"
}The request went to port 3511; Next.js attached the credential and forwarded it to port 3531.
Step 22. Create the Client Component
Create main/src/app/(landing)/tutorial/hello-world/_components/hello-image-generator.tsx:
'use client';
import Image from 'next/image';
import {FormEvent, useEffect, useState} from 'react';
type TutorialStatus = 'idle' | 'queued' | 'processing' | 'completed' | 'failed';
type JobResponse = {
success: boolean;
job_id?: string;
status?: Exclude<TutorialStatus, 'idle'>;
message?: string;
};
export function HelloImageGenerator() {
const [text, setText] = useState('Hello, Marsad!');
const [jobId, setJobId] = useState<string | null>(null);
const [status, setStatus] = useState<TutorialStatus>('idle');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!jobId || !['queued', 'processing'].includes(status)) {
return;
}
const timer = window.setInterval(async () => {
try {
const response = await fetch(
`/api/tutorial/hello-image/${jobId}`,
{cache: 'no-store'},
);
const data = (await response.json()) as JobResponse;
if (!response.ok || !data.status) {
setStatus('failed');
setError(data.message ?? 'Could not read the job status.');
return;
}
setStatus(data.status);
if (data.status === 'failed') {
setError(data.message ?? 'Image generation failed.');
}
} catch {
setStatus('failed');
setError('Could not reach the job status endpoint.');
}
}, 1000);
return () => window.clearInterval(timer);
}, [jobId, status]);
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
setJobId(null);
setStatus('queued');
try {
const response = await fetch('/api/tutorial/hello-image', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text}),
});
const data = (await response.json()) as JobResponse;
if (!response.ok || !data.job_id) {
setStatus('failed');
setError(data.message ?? 'Could not enqueue the image job.');
return;
}
setJobId(data.job_id);
setStatus(data.status ?? 'queued');
} catch {
setStatus('failed');
setError('Could not reach the Web App API.');
}
}
return (
<div className='mt-8 w-full max-w-2xl rounded-xl border p-6 text-left'>
<form className='space-y-4' onSubmit={handleSubmit}>
<div className='space-y-2'>
<label className='text-sm font-medium' htmlFor='hello-text'>
Image text
</label>
<input
id='hello-text'
value={text}
maxLength={80}
onChange={(event) => setText(event.target.value)}
className='w-full rounded-md border bg-background px-3 py-2'
/>
</div>
<button
type='submit'
disabled={!text.trim() || ['queued', 'processing'].includes(status)}
className='rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50'
>
Generate image
</button>
</form>
<div className='mt-6 space-y-2' aria-live='polite'>
<p>Status: {status}</p>
{jobId ? <p className='break-all text-sm'>Job: {jobId}</p> : null}
{error ? <p className='text-sm text-destructive'>{error}</p> : null}
</div>
{status === 'completed' && jobId ? (
<Image
src={`/api/tutorial/hello-image/${jobId}/image`}
alt={`Generated image containing: ${text}`}
width={1200}
height={630}
unoptimized
className='mt-6 h-auto w-full rounded-lg border'
/>
) : null}
</div>
);
}Step 23. Add it to the Server Component page
In main/src/app/(landing)/tutorial/hello-world/page.tsx, import:
import {HelloImageGenerator} from './_components/hello-image-generator';Render it below the existing Hello Marsad text:
<HelloImageGenerator />Keep page.tsx as a Server Component. The page retains its metadata export; only the interactive generator needs 'use client'.
Step 24. Run the complete flow
Open http://localhost:3511/tutorial/hello-world, enter a greeting, and select Generate image.
Expected output
The page visibly progresses through:
Status: queued
Job: hello-image-...then:
Status: processingand finally:
Status: completedThe generated PNG appears below the status.
Group 5 checkpoint
Generate a new image while watching the browser Network panel, the Model Server terminal, and the worker terminal. Show your onboarding partner the same job ID at each boundary.
Group 6: Exercise Failure Paths
Step 25. Reject invalid text
Submit an empty value directly to the Next.js API or temporarily remove the browser's disabled guard.
Expected output
{
"success": false,
"message": "Text must contain between 1 and 80 characters."
}The response status is 422, and no job ID is created.
Step 26. Observe a stopped worker
Stop only the tasks worker and submit a valid job.
Expected output
The page remains at queued; it must not claim that the image completed. Restart the worker and confirm that the same job progresses to completed.
Step 27. Confirm authentication
Call the Model Server job endpoint directly without a bearer header:
POST http://localhost:3531/api/tutorial/hello-imageExpected output
401 UnauthorizedSubmitting through the Next.js page still succeeds because the Next.js server attaches MODEL_API_TOKEN.
Step 28. Run focused validation
From model-server/:
pytest tests/test_hello_route.py tests/test_hello_image_task.pyFrom main/:
npx tsc --noEmitGroup 6 checkpoint
Valid work completes, invalid text returns 422, direct unauthenticated access returns 401, and a stopped worker leaves the job honestly queued until the worker returns.
Group 7: Explain and Clean Up
Before removing anything, explain:
- Why the browser calls Next.js rather than the Model Server.
- Why enqueue returns
202instead of waiting for the PNG. - How the job ID connects the HTTP request, Redis, worker, status endpoint, and output.
- Why the browser cannot provide an output path.
- Why a Client Component is needed only for the interactive generator.
- How this polling tutorial differs from Marsad's persistent jobs and authenticated completion callbacks.
Then remove only the tutorial artifacts:
Web App
src/app/(landing)/tutorial/hello-world/src/app/api/tutorial/model-hello/src/app/api/tutorial/hello-image/src/app/api/tutorial/_lib/model-server.tsif the_libfolder contains nothing else
Model Server
src/routes/hello.pysrc/models/hello_image_task.pytests/test_hello_route.pytests/test_hello_image_task.py- the
hello_bpimport, registration, and protection entry inapp.py
Generated data
- only the PNG files created beneath
shared-data/generated/tutorial/
Do not recursively remove shared-data/generated/, src/routes/, src/models/, tests/, or the parent Next.js tutorial folders if they contain unrelated work.
Run git status --short in the Marsad repository root. No Hello tutorial source file or registration change should remain.
Final Checkpoint
You have completed 3C when you can draw the full request path, identify the owner of every state transition, reproduce both success and failure, and return the working tree to its pre-tutorial state.