Shopify's app development templates are based on React Router, while Polaris's standard style is to use Web Components ( <s-*> ) .
In this tutorial, we will update Prisma (SQLite) included in the template and create an app that creates, lists, and scans QR codes by calling the Shopify Admin API.
Official documentation: https://shopify.dev/docs/apps/build/build?framework=reactRouter
What you will do in this tutorial
The official tutorial's goal is as follows:
-
Update the Prisma DB (template included)
Authenticate with
@shopify/shopify-app-react-routerand get product information with Admin API (GraphQL)Create a screen using Polaris Web Components (
<s-page>,<s-section>, etc.)
1. Start developing a Shopify app | Create an app using the official Shopify template (React Router)
Here, we will start development using the app template provided by Shopify .
Authentication and embedding settings in the admin panel are included from the start.
Create an app
npm init @shopify/app@latest
When you run this command, you will be asked a few interactive questions.
Framework → Select React Router → This is the configuration currently recommended by Shopify.
Simply answer the questions and a set of files required for app development will be automatically generated.
Launch the created app
cd your-app
npm install
npm run dev
Their respective roles are as follows:
cd your-app
Go to the created app foldernpm install
Install the libraries required to run the app.npm run dev
→ Start the development server and connect the Shopify admin panel to the app
Run npm run dev and the server will start.

Open the Preview URL displayed in the terminal and the app will appear in your Shopify admin panel .
2. Database design for Shopify app development | Adding a QRCode table to the database (Prisma)
In this tutorial, we will save the QR code settings (which product it is linked to, destination, title, number of scans, etc.) in the database .
The official Shopify template (React Router version) is configured to use Prisma for database operations, so we first define the "destination table."
What is Prisma?
Write a DB blueprint (schema)
Create a table in the database from the blueprint.
This is a tool that allows you to safely operate a database from Node.js.
Add QRCode model to schema.prisma
prisma/schema.prisma is the file that defines the DB structure.
Adding QRCode here means "create a table called QRCode."
prisma/schema.prisma (final version)
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:dev.sqlite"
}
model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
scope String?
expires DateTime?
accessToken String
userId BigInt?
firstName String?
lastName String?
email String?
accountOwner Boolean @default(false)
locale String?
collaborator Boolean? @default(false)
emailVerified Boolean? @default(false)
}
model QRCode {
id Int @id @default(autoincrement())
shop String
title String
productId String
productHandle String
productVariantId String
destination String
scans Int @default(0)
createdAt DateTime @default(now())
}
Contents defined here (only the main points)
id: Primary key with a sequential number (increases automatically)shop: Which store's data is this? ( Required for multi-store support )title: Name for management purposesproductId / productVariantId: which product (variant)-
destination: The type of destination the QR code will take you to (e.g. product page / checkout, etc.) scans: Number of scans (default 0)createdAt: Creation date and time (automatically entered)
Error in the official documentation
The official documentation defines only the essential columns. In reality, columns such as firstName and email are also required.
Previously, the Remix library only allowed you to use the essentials, but this has changed since the React Router v7 framework was released.
Please note that the official documentation does not reflect this change.
Migration (reflecting in DB)
Simply writing schema.prisma will not change the database.
Create a table in the actual DB (SQLite) with the following command:
npx prisma migrate dev --name add_qrcode
migrate dev: Reflect to DB for development (update local DB)--name add_qrcode: Note of "what change was made" (history name)
Check the contents of the database with Prisma Studio
Prisma provides an administration screen that allows you to view and edit the contents of the database using a browser .
That's Prisma Studio .
npm run prisma studio
This command will open the following URL in your browser:
http://localhost:5555


What can you do with Prisma Studio?
With Prisma Studio you can:
Check the contents of the QRCode table in a list
Visually check the actual data stored
Manually add, modify, or delete data during development
-
Instantly check whether the data is properly saved in the database
👉The biggest advantage is that you can check the database without writing any code .
3. Server implementation of Shopify app | Implementing the QR code model layer (server-side logic)
app/models/QRCode.server.js we will create here is, in a nutshell, the "core of the server side of the QR code function."
Read/save QR code information from DB (Prisma)
Get product information using Shopify Admin API (GraphQL) and "complete it for display"
Generate a QR code image (Data URL)
Validating form input
In other words, from the perspective of the screen (routes), it looks like "please ask here to do all the necessary processing."
app/models/QRCode.server.js (finished version)
// app/models/QRCode.server.js
import qrcode from "qrcode";
import invariant from "tiny-invariant";
import db from "../db.server";
/**
* 1 item acquired (DB → Complemented with Shopify product information → Image generation)
*/
export async function getQRCode(id, graphql) {
const qrCode = await db.qRCode.findFirst({ where: { id } });
if (!qrCode) return null;
return supplementQRCode(qrCode, graphql);
}
/**
* List acquisition (DB → complete each line)
*/
export async function getQRCodes(shop, graphql) {
const qrCodes = await db.qRCode.findMany({
where: { shop },
orderBy: { id: "desc" },
});
if (qrCodes.length === 0) return [];
return Promise.all(qrCodes.map((qrCode) => supplementQRCode(qrCode, graphql)));
}
/**
* Generate QR code image (Base64 Data URL)
* * A format that can be pasted directly into the browser as <img src="...">
*/
export function getQRCodeImage(id) {
const url = new URL(process.env.SHOPIFY_APP_URL);
url.pathname = `/qrcodes/${id}`;
return qrcode.toDataURL(url.href);
}
/**
* Decide the "destination URL" where the QR code will jump
*/
export function getDestinationUrl(qrCode) {
switch ( qrCode . destination ) {
case "product":
return `https://${qrCode.shop}/products/${qrCode.productHandle}`;
case "checkout":
return `https://${qrCode.shop}/cart/${qrCode.productVariantId}:1`;
default:
return `https://${qrCode.shop}`;
}
}
/**
* Call the Shopify Admin GraphQL API
* Add "product information required for display" to the QR Code information in the database
*/
async function supplementQRCode(qrCode, graphql) {
const query = `#graphql
query supplementQRCode($id: ID!) {
product(id: $id) {
title
handle
images(first: 1) {
edges {
node {
url
altText
}
}
}
}
}
`;
const response = await graphql(query, {
variables: { id: qrCode.productId },
});
const {
data: { product },
} = await response.json();
// Prepare for the case where the product has disappeared (or there is insufficient permission, etc.)
const productDeleted = !product?.title;
const productHandle = product?.handle || qrCode.productHandle || "";
return {
...qrCode,
// Arrange it in a way that is easy to use on the screen
productDeleted,
productTitle: product?.title || "",
productHandle,
productImage: product?.images?.edges?.[0]?.node?.url || null,
productAlt: product?.images?.edges?.[0]?.node?.altText || "",
// Create and pass the "destination URL" and "QR image" here.
destinationUrl: getDestinationUrl({ ...qrCode, productHandle }),
image: await getQRCodeImage(qrCode.id),
};
}
/**
* Input check (assuming it is called from a routes action)
* Errors are returned in the form { field name: message }
*/
export function validateQRCode(data) {
const errors = {};
if (!data.title) errors.title = "Title is required";
if (!data.productId) errors.productId = "Product is required";
if (!data.destination) {
errors.destination = "Destination is required";
} else if (!["product", "checkout"].includes(data.destination)) {
errors.destination = "Destination must be 'product' or 'checkout'";
}
return errors;
}
/**
* Handle creation and updates together (Upsert style)
* - update if id exists
* - create if not found
*/
export async function upsertQRCode({ id, shop, data }) {
invariant(shop, "shop is required");
const payload = {
shop,
title: data.title,
productId: data.productId,
productHandle: data.productHandle || "",
productVariantId: data.productVariantId,
destination: data.destination,
};
if (id) {
return db.qRCode.update({
where: { id },
data: payload,
});
}
return db.qRCode.create({
data: payload,
});
}
/**
* delete
*/
export async function deleteQRCode(id) {
return db.qRCode.delete({ where: { id } });
}
Function explanation (very important points)
-
db.qRCode.findMany(...)is the part that queries the database using Prisma (SQL). graphql(query, ...)is the part that retrieves products using the Shopify Admin API (GraphQL).supplementQRCode()is a convenient function that compiles "display information that is missing from the DB alone"qrcode.toDataURL()returns the QR code as a string without creating an image file (which can be pasted directly into HTML)
Learn Shopify app development tutorials through videos!
If you don't understand the text, please refer to the explanatory video uploaded to YouTube.
4. Shopify App Admin Screen Development | Admin Screen: Create a QR List Page (/app)
In this step, a "QR Code List" will be displayed at the top of the admin screen ( /app ).
Get the list of QR codes from the database on the server side (loader)
Display a list table or an empty state on the screen (React component)
s-*components are the UI for the admin screen provided by Shopify (the appearance is neat from the start)
What this file is responsible for (overall picture)
app/routes/app._index.jsx is the "top page of /app".
loader(): Before displaying the page, identify the logged-in shop and retrieve a list of QR codes.Index(): Receives the result of the loader. If there are no results, it will be empty. If there are results, it will draw a table.
Key points about loader
export async function loader({ request }) {
const { admin, session } = await authenticate.admin(request);
const qrCodes = await getQRCodes(session.shop, admin.graphql);
return { qrCodes };
}
-
authenticate.admin(request)Check if the access is from the Shopify admin screen, and if it's OK
session.shop(which store) andadmin.graphql(GraphQL client for accessing the Admin API)
It will return
-
getQRCodes(session.shop, admin.graphql)Get
shop's QR Code from the DB (Prisma)Furthermore, product information (title and image) is also completed and returned using GraphQL → This allows you to display "product name and image" in a list.
About s-* components
s-* like <s-page> and <s-table> is a UI provided by Shopify for the admin screen and is called Polaris Web Component.
Margins, font size, and appearance
Empty State
-
Table View
It will be finished in the same way as the “Shopify admin panel design”.
The first advantage of using Polaris is that you can create functions without worrying about the details of the UI .
app/routes/app._index.jsx (completed version)
// app/routes/app._index.jsx
import { useLoaderData, Link } from "react-router";
import { boundary } from "@shopify/shopify-app-react-router/server";
import { authenticate } from "../shopify.server";
import { getQRCodes } from "../models/QRCode.server";
export async function loader({ request }) {
const { admin, session } = await authenticate.admin(request);
const qrCodes = await getQRCodes(session.shop, admin.graphql);
return { qrCodes };
}
const EmptyQRCodeState = () => (
<s-section accessibilityLabel="Empty state section">
<s-grid gap="base" justifyItems="center" paddingBlock="large-400">
<s-box maxInlineSize="200px" maxBlockSize="200px">
<s-image
aspectRatio="1/0.5"
src="https://cdn.shopify.com/s/files/1/0262/4071/2726/files/emptystate-files.png"
alt="A stylized graphic of a document"
/>
</s-box>
<s-grid justifyItems="center" maxBlockSize="450px" maxInlineSize="450px">
<s-heading>Create unique QR codes for your products</s-heading>
<s-paragraph>
Allow customers to scan codes and buy products using their phones.
</s-paragraph>
<s-stack
gap="small-200"
justifyContent="center"
padding="base"
paddingBlockEnd="none"
direction="inline"
>
<s-button href="/app/qrcodes/new" variant="primary">
Create QR code
</s-button>
</s-stack>
</s-grid>
</s-grid>
</s-section>
);
function truncate(str, { length = 25 } = {}) {
if (!str) return "";
if (str.length <= length) return str;
return str.slice(0, length) + "…";
}
const QRTable = ({ qrCodes }) => (
<s-section padding="none" accessibilityLabel="QRCode table">
<s-table>
<s-table-header-row>
<s-table-header listSlot="primary">Title</s-table-header>
<s-table-header>Product</s-table-header>
<s-table-header>Date created</s-table-header>
<s-table-header>Scans</s-table-header>
</s-table-header-row>
<s-table-body>
{qrCodes.map((qrCode) => (
<QRTableRow key={qrCode.id} qrCode={qrCode} />
))}
</s-table-body>
</s-table>
</s-section>
);
const QRTableRow = ({ qrCode }) => (
<s-table-row id={qrCode.id} position={qrCode.id}>
<s-table-cell>
<s-stack direction="inline" gap="small" alignItems="center">
<s-clickable
href={`/app/qrcodes/${qrCode.id}`}
accessibilityLabel={`Go to the product page for ${qrCode.productTitle}`}
border="base"
borderRadius="base"
overflow="hidden"
inlineSize="20px"
blockSize="20px"
>
{qrCode.productImage ? (
<s-image objectFit="cover" src={qrCode.productImage}></s-image>
) : (
<s-icon size="large" type="image" />
)}
</s-clickable>
<s-link href={`/app/qrcodes/${qrCode.id}`}>
{truncate(qrCode.title)}
</s-link>
</s-stack>
</s-table-cell>
<s-table-cell>
{qrCode.productDeleted ? (
<s-badge icon="alert-diamond" tone="critical">
Product has been deleted
</s-badge>
) : (
truncate(qrCode.productTitle)
)}
</s-table-cell>
<s-table-cell>{new Date(qrCode.createdAt).toDateString()}</s-table-cell>
<s-table-cell>{qrCode.scans}</s-table-cell>
</s-table-row>
);
export default function Index() {
const { qrCodes } = useLoaderData();
return (
<s-page heading="QR codes">
<s-link slot="secondary-actions" href="/app/qrcodes/new">
Create QR code
</s-link>
{qrCodes.length === 0 ? <EmptyQRCodeState /> : <QRTable qrCodes={qrCodes} />}
</s-page>
);
}
export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};
Empty state
State where data exists
5. Shopify app admin screen development | Admin screen: QR code creation and editing page (/app/qrcodes/:id)
The key point of this page is that you can create new files and edit them in the same file .
If the URL is
/app/qrcodes/new, create new mode-
When the URL is
/app/qrcodes/123→ Edit mode
By using React Router's "loader/action",
Screen display (loader) and saving/deleting (action) can be combined into this one file.
What page are you on?
-
loader
If
new, returns empty initial dataIf it's a number, retrieve the QR code from the database, complete it with product information using GraphQL, and return it (
getQRCode()).
-
action
If
action=delete, delete and return to listOtherwise, save (validate → upsert → redirect to saved URL)
-
UI
Use
useSubmit()to execute "Save" and "Delete" like a form submission (you can sendFormDatawithout placing<form>).
app/routes/app.qrcodes.$id.jsx (completed version)
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
shop,
};
if (data.action === "delete") {
await db.qRCode.delete({ where: { id: Number(params.id) } });
return redirect("/app");
}
const errors = validateQRCode(data);
if (errors) {
return new Response(JSON.stringify({ errors }), {
status: 422,
headers: {
"Content-Type": "application/json",
},
});
}
const qrCode =
params.id === "new"
? await db.qRCode.create({ data })
: await db.qRCode.update({ where: { id: Number(params.id) }, data });
return redirect(`/app/qrcodes/${qrCode.id}`);
}
export default function QRCodeForm() {
const navigate = useNavigate();
const { id } = useParams();
const qrCode = useLoaderData();
const [initialFormState, setInitialFormState] = useState(qrCode);
const [formState, setFormState] = useState(qrCode);
const errors = useActionData()?.errors || {};
// const isSaving = useNavigation().state === "submitting";
const isDirty =
JSON.stringify(formState) !== JSON.stringify(initialFormState);
async function selectProduct() {
const products = await window.shopify.resourcePicker({
type: "product",
action: "select", // customized action verb, either 'select' or 'add',
});
if (products) {
const { images, id, variants, title, handle } = products[0];
setFormState({
...formState,
productId: id,
productVariantId: variants[0].id,
productTitle: title,
productHandle: handle,
productAlt: images[0]?.altText,
productImage: images[0]?.originalSrc,
});
}
}
function removeProduct() {
setFormState({
title: formState.title,
destination: formState.destination,
});
}
const productUrl = formState.productId
? `shopify://admin/products/${formState.productId.split("/").at(-1)}`
: "";
const submit = useSubmit();
function handleSave(e) {
e.preventDefault(); // Add
const data = {
title: formState.title,
productId: formState.productId || "",
productVariantId: formState.productVariantId || "",
productHandle: formState.productHandle || "",
destination: formState.destination,
};
submit(data, { method: "post" });
}
function handleDelete() {
submit({ action: "delete" }, { method: "post" });
}
function handleReset() {
setFormState(initialFormState);
window.shopify.saveBar.hide("qr-code-form");
}
// useEffect(() => {
// if (isDirty) {
// window.shopify.saveBar.show("qr-code-form");
// } else {
// window.shopify.saveBar.hide("qr-code-form");
// }
// return () => {
// window.shopify.saveBar.hide("qr-code-form");
// };
// }, [isDirty]);
useEffect(() => {
setInitialFormState(qrCode);
setFormState(qrCode);
}, [id, qrCode]);
return (
<>
<form data-save-bar onSubmit={(e) => handleSave(e)} onReset={handleReset}>
<s-page heading={initialFormState.title || "Create QR code"}>
<s-link
href="/app"
slot="breadcrumb-actions"
onClick={(e) => (isDirty ? e.preventDefault() : navigate("/app/"))}
>
QR Codes
</s-link>
{initialFormState.id &&
<s-button slot="secondary-actions" onClick={handleDelete}>Delete</s-button>}
<s-section heading="QR Code information">
<s-stack gap="base">
<s-text-field
label="Title"
details="Only store staff can see this title"
error={errors.title}
autoComplete="off"
name="title"
value={formState.title}
onInput={(e) =>
setFormState({ ...formState, title: e.target.value })
}
></s-text-field>
<s-stack gap="500" align="space-between" blockAlign="start">
<s-select
name="destination"
label="Scan destination"
value={formState.destination}
onChange={(e) =>
setFormState({ ...formState, destination: e.target.value })
}
>
<s-option
value="product"
selected={formState.destination === "product"}
>
Link to product page
</s-option>
<s-option
value="cart"
selected={formState.destination === "cart"}
>
Link to checkout page with product in the cart
</s-option>
</s-select>
{initialFormState.destinationUrl ? (
<s-link
variant="plain"
href={initialFormState.destinationUrl}
target="_blank"
>
Go to destination URL
</s-link>
) : null}
</s-stack>
<s-stack gap="small-400">
<s-stack direction="inline" gap="small-100" justifyContent="space-between">
<s-text color="subdued">Product</s-text>
{formState.productId ? (
<s-link
onClick={removeProduct}
accessibilityLabel="Remove the product from this QR Code"
variant="tertiary"
tone="neutral"
>
Clear
</s-link>
) : null}
</s-stack>
{formState.productId ? (
<s-stack
direction="inline"
justifyContent="space-between"
alignItems="center"
>
<s-stack
direction="inline"
gap="small-100"
alignItems="center"
>
<s-clickable
href={productUrl}
target="_blank"
accessibilityLabel={`Go to the product page for ${formState.productTitle}`}
borderRadius="base"
>
<s-box
padding="small-200"
border="base"
borderRadius="base"
background="subdued"
inlineSize="38px"
blockSize="38px"
>
{formState.productImage ? (
<s-image src={formState.productImage}></s-image>
) : (
<s-icon size="large" type="product" />
)}
</s-box>
</s-clickable>
<s-link href={productUrl} target="_blank">
{formState.productTitle}
</s-link>
</s-stack>
<s-stack direction="inline" gap="small">
<s-button
onClick={selectProduct}
accessibilityLabel="Change the product the QR code should be for"
>
Change
</s-button>
</s-stack>
</s-stack>
) : (
<s-button
onClick={selectProduct}
accessibilityLabel="Select the product the QR code should be for"
>
Select product
</s-button>
)}
</s-stack>
</s-stack>
</s-section>
<s-box slot="aside">
<s-section heading="Preview">
<s-stack gap="base">
<s-box
padding="base"
border="none"
borderRadius="base"
background="subdued"
>
{initialFormState.image ? (
<s-image
aspectRatio="1/0.8"
src={initialFormState.image}
alt="The QR Code for the current form"
/>
) : (
<s-stack
direction="inline"
alignItems="center"
justifyContent="center"
blockSize="198px"
>
<s-text color="subdued">
See a preview once you save
</s-text>
</s-stack>
)}
</s-box>
<s-stack
gap="small"
direction="inline"
alignItems="center"
justifyContent="space-between"
>
<s-button
disabled={!initialFormState.id}
href={`/qrcodes/${initialFormState.id}`}
target="_blank"
>
Go to public URL
</s-button>
<s-button
disabled={!initialFormState?.image}
href={initialFormState?.image}
download
variant="primary"
>
Download
</s-button>
</s-stack>
</s-stack>
</s-section>
</s-box>
</s-page>
</form>
</>
);
}
export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
}

Error in the official documentation
There is a serious bug in the official documentation.
The submit function is executed within the onSubmit of the form tag.
This means that after submitting (sending a request) using the form tag, you will again submit (send a request) using the submit function.
The form tag request submission does not contain any information that the backend can use to verify that the request is secure.
This causes an authentication error.
Here, we have made the correction that the form tag request sending is canceled with e.preventDefault() and the request is sent using the submit function, which can send a request including authentication information.
Please note that when sending a request to the backend, you need to use useSubmit or useFetch to send a request including authentication information.
6. Shopify App Public Page Development: Public Page: Display QR Code Image (/qrcodes/:id)
This page does not require login (non-Admin) .
Since the person who scans the QR code may not be logged in to the Shopify admin panel, we will implement this as an unauthenticated route .
When you actually access it, you will see the following image outside of the Shopify admin screen.

app/routes/qrcodes.$id.jsx (completed version)
import invariant from "tiny-invariant";
import { useLoaderData } from "react-router";
import db from "../db.server";
import { getQRCodeImage } from "../models/QRCode.server";
export const loader = async ({ params }) => {
invariant(params.id, "Could not find QR code destination");
const id = Number(params.id);
const qrCode = await db.qRCode.findFirst({ where: { id } });
invariant(qrCode, "Could not find QR code destination");
return {
title: qrCode.title,
image: await getQRCodeImage(id),
};
};
export default function QRCode() {
const { image, title } = useLoaderData();
return (
<>
<h1>{title}</h1>
<img src={image} alt={`QR Code for product`} />
</>
);
}
7. Develop a route dedicated to Shopify app processing: Scan route: Increment the number of scans by 1 and redirect to the destination URL (/qrcodes/:id/scan)
This route is the URL that will be accessed the moment the QR code is scanned .
It is used as a route dedicated to processing , not as a page for displaying a screen.
When this URL is accessed, it does the following:
Check if the QR code ID (
:id) is correctGet the corresponding QR code from the database
Increase the number of scans
scans1Immediate redirection to the URL according to the QR code settings
In other words,
👉 "Record the scan and then send the user to the product page or checkout."
This is the route for.
app/routes/qrcodes.$id.scan.jsx (completed version)
// app/routes/qrcodes.$id.scan.jsx
import { redirect } from "react-router";
import invariant from "tiny-invariant";
import db from "../db.server";
import { getDestinationUrl } from "../models/QRCode.server";
export const loader = async ({ params }) => {
invariant(params.id, "Could not find QR code destination");
const id = Number(params.id);
const qrCode = await db.qRCode.findFirst({ where: { id } });
invariant(qrCode, "Could not find QR code destination");
await db.qRCode.update({
where: { id },
data: { scans: { increment: 1 } },
});
return redirect(getDestinationUrl(qrCode));
};
Why use a loader?
The loader is a server process that runs before the page is displayed.
Since there is no need to return the screen, it is ideal for applications that only perform "count → redirect"
Since no front-end JS is required,
👉 Works reliably even when accessed from a smartphone, QR scanner, or external browser
8. Operation check list
Once you have implemented this, please check the following in order from top to bottom .
If everything is OK, the QR code management app is working properly.
Management screen (in Shopify Admin)
When you access
/app, a list of QR codes will be displayed.Click "Create QR code" to open
/app/qrcodes/newYou can enter the required information and save it.
After saving, you will automatically be redirected to
/app/qrcodes/:id(edit screen)The QR code image and public URL will be displayed on the editing screen.
Public page (outside Admin)
/qrcodes/:idcan be accessed directly (no login required)The QR code image will be displayed.
Scan Operation
When I access
/qrcodes/:id/scan, no error occurs.-
Each time you access it,
scansvalue increases by +1.Check with Prisma Studio (
http://localhost:5555)
-
Depending on
destinationyou setProduct page or
-
You will be redirected to checkout
Frequently asked questions
-
The URL of the QR code is
{APP_URL}/qrcodes/:id/scanIs it becoming
Is
SHOPIFY_APP_URLset correctly in.env?Is there data in
QRCodetable in Prisma Studio?
Once this checklist is complete,
The "Shopify App Development Official Tutorial (React Router Edition)" is complete 🎉
9. Conclusion | For those who want to seriously learn Shopify app development
In this article, we will introduce the latest Shopify app development tutorial using the official Shopify template (React Router version) .
Initial app construction
DB design (Prisma)
Implementing the admin app
Public pages and scanning
Practical composition ideas
I have explained everything up to that point.
If you understand this configuration,
The “big picture of Shopify app development” should now be pretty clear.
However, in practice,
Product selection UI (Resource Picker)
Billing (paid app)
Webhook / Flow / Functions integration
Security and Operational Design
Design decisions based on actual projects
There are many points that are difficult to grasp from the official documentation alone , such as:
If you want to learn Shopify app development systematically, try Tech Geek
If you are
I want to work as a Shopify app developer
I feel limited by self-study
I want to acquire design skills that can be used in practice
If you are thinking,
TechGeek , an online school specializing in Shopify app development, is another option.
At Tech Geek,
Shopify app design at a real project level
-
Latest official configuration (React Router/Web Components)
How to become an engineer who can not only "create" but also "make proposals"
The curriculum is designed with an emphasis on
If you understand the contents of this article,
You're now ready to take the next step and get started with more hands-on Shopify app development .
👉 [Official Website] TechGeek - Shopify App Development: https://techgeek-school.com/