Web app tutorial 

Estimated time: 10 minutes

In this tutorial you will build an app that creates and displays custom Gadgémon. Custom sprites for your Gadgémon will be generated using OpenAI's DALL-E API and Gadget's built-in OpenAI connection.

What you will learn 

After finishing this tutorial, you will know how to:

Step 1: Create a new Gadget app 

Every Gadget app includes a hosted Postgres database, a serverless Node backend, and a Vite + React frontend.

  1. Start by creating a new Gadget app at https://gadget.new
  2. Select the Web app type. Make sure to use the Yes, enable auth option
  3. Click the Continue button
  4. Enter an app name and create the app

Step 2: Build your model 

Models in Gadget work similarly to models in an ORM (object relational mapper) and map to tables in a database. They have one or more fields which are columns in your tables and are used to store your application data.

Create a new model to store your Gadgémon:

  1. Click the + button next to api/models to create a new model
  2. Name the model gadgemon and add the following fields:
  • name: a string field
  • similar: a string field
  • element: an enum field with options grass, fire, and water
  • sprite: a file field
  1. Add a Required validation to name, similar, and element. The action needs all three values to generate a sprite.
A screenshot of the gadgemon model, with name, similar, element, and sprite fields. The enum field is focused, with the options set to grass, fire, and water.
More on models

To learn more about models, see the models documentation.

Step 3: Add the OpenAI plugin 

Gadget has built-in plugins you can use to connect your app with external services, such as OpenAI.

You can add the OpenAI plugin to your app by following these steps:

  1. Click on Settings in the sidebar
  2. Click on Plugins
  3. Select OpenAI from the list of plugins
  4. Leave the default Gadget development keys selected and click Add connection
More on plugins

You can now use the OpenAI connection in your app to generate sprites for your Gadgémon. To learn more about plugins check out the plugins documentation.

Step 4: Write your backend action 

As you build models in Gadget, a CRUD (create, read, update, delete) API is automatically generated. You can see these actions and the code that powers them in the api/models/gadgemon/actions folder.

Now add code to your gadgemon.create action that generates a sprite for your Gadgémon using the OpenAI plugin:

  1. Go to api/model/gadgemon/actions/create.js
  2. Paste the following code (replace the entire file):
api/models/gadgemon/actions/create.js
JavaScript
import { applyParams, save, ActionOptions } from "gadget-server"; import { preventCrossUserDataAccess } from "gadget-server/auth"; export const run: ActionRun = async ({ params, record, logger, api }) => { applyParams(params, record); await preventCrossUserDataAccess(params, record); await save(record); }; export const onSuccess: ActionOnSuccess = async ({ params, record, logger, api, connections, }) => { // "record" is the newly created Gadgemon, with name, similar and element fields that will be added by the user const { id, name, similar, element } = record; // prompt sent to OpenAI to generate the Gadgemon sprite const prompt = `A pixel art style pokemon sprite named ${name} that looks similar to a ${similar} that is a ${element} element. Do not include any text, including the name, in the image`; // call the OpenAI images generate (DALL-E) API: https://github.com/openai/openai-node/blob/v4/src/resources/images.ts const response = await connections.openai.images.generate({ prompt, n: 1, size: "256x256", response_format: "url", }); const imageUrl = response.data?.[0]?.url; // write to the Gadget Logs logger.info({ imageUrl }, `Generated image URL for Gadgemon id ${id}`); // save the image file to the newly created Gadgémon record await api.gadgemon.update(id, { gadgemon: { sprite: { copyURL: imageUrl, }, }, }); }; export const options: ActionOptions = { actionType: "create", timeoutMS: 60000, };
import { applyParams, save, ActionOptions } from "gadget-server"; import { preventCrossUserDataAccess } from "gadget-server/auth"; export const run: ActionRun = async ({ params, record, logger, api }) => { applyParams(params, record); await preventCrossUserDataAccess(params, record); await save(record); }; export const onSuccess: ActionOnSuccess = async ({ params, record, logger, api, connections, }) => { // "record" is the newly created Gadgemon, with name, similar and element fields that will be added by the user const { id, name, similar, element } = record; // prompt sent to OpenAI to generate the Gadgemon sprite const prompt = `A pixel art style pokemon sprite named ${name} that looks similar to a ${similar} that is a ${element} element. Do not include any text, including the name, in the image`; // call the OpenAI images generate (DALL-E) API: https://github.com/openai/openai-node/blob/v4/src/resources/images.ts const response = await connections.openai.images.generate({ prompt, n: 1, size: "256x256", response_format: "url", }); const imageUrl = response.data?.[0]?.url; // write to the Gadget Logs logger.info({ imageUrl }, `Generated image URL for Gadgemon id ${id}`); // save the image file to the newly created Gadgémon record await api.gadgemon.update(id, { gadgemon: { sprite: { copyURL: imageUrl, }, }, }); }; export const options: ActionOptions = { actionType: "create", timeoutMS: 60000, };

This code will run every time your gadgemon.create API action is called.

Test your action 

Gadget apps include an API playground that can be used to test your actions.

Use the playground to run your code in the gadgemon.create action:

  1. While in api/models/gadgemon/actions/create.js, click the Run Action button.
  2. Use the playground's API client to create a Gadgémon:
call create action in playground
JavaScript
await api.gadgemon.create({ name: "Gadgetbot", similar: "robot", element: "grass", user: { _link: "1", }, });
await api.gadgemon.create({ name: "Gadgetbot", similar: "robot", element: "grass", user: { _link: "1", }, });
  1. Click the execute query button to run the action

You should see a success: true response in the playground, which means you created a new gadgemon record.

  1. Go to api/models/gadgemon/data to view your model records
More on actions

Actions define your application's API. To learn more about actions, see the actions guide.

Step 5: Build your frontend 

You need an interface to create and display your Gadgémon. In Gadget, your Vite + React frontend is found inside the web folder, and an API client has been set up for you in web/api.js.

You will use this client and Gadget's React hooks to call your gadgemon.create action and read gadgemon model records.

  1. Go to web/routes/_app.signed-in.jsx and replace the contents with the following code:
web/routes/_app.signed-in.jsx
React
import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { api } from "../api"; import { useFindMany } from "@gadgetinc/react"; import { AutoForm } from "@/components/auto"; const elementColors: Record<string, string> = { grass: "bg-green-100 text-green-800", fire: "bg-red-100 text-red-800", water: "bg-blue-100 text-blue-800", }; export default function () { const [{ data: gadgemon, error, fetching }] = useFindMany(api.gadgemon, { select: { id: true, name: true, sprite: { url: true }, element: true, similar: true, }, live: true, }); return ( <div className="container mx-auto px-4 py-2 space-y-6"> <div className="bg-white rounded-2xl border shadow-sm p-6"> <h2 className="text-2xl font-bold mb-4">Create a Gadgemon</h2> <AutoForm action={api.gadgemon.create} include={["name", "similar", "element"]} title="" /> </div> <div> <h2 className="text-2xl font-bold mb-6">Gadgedex</h2> {error && ( <Alert variant="destructive" className="mb-6"> <AlertDescription>Failed to load Gadgemon: {error.message}</AlertDescription> </Alert> )} {fetching && !gadgemon && <p className="text-gray-400">Loading...</p>} {!fetching && !error && gadgemon?.length === 0 && <p className="text-gray-400">No Gadgemon yet. Create one above!</p>} {gadgemon && gadgemon.length > 0 && ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {gadgemon.map(({ id, name, sprite, element, similar }) => ( <Card key={id} className="overflow-hidden hover:shadow-md transition-shadow"> <div className="aspect-square bg-gray-50 flex items-center justify-center text-4xl text-gray-300"> {sprite?.url ? <img src={sprite.url} alt={name || "Gadgemon"} className="w-full h-full object-cover" /> : "🎭"} </div> <CardContent className="p-4"> <div className="flex items-center justify-between mb-1"> <h3 className="font-semibold truncate">{name || "Unnamed Gadgemon"}</h3> <Badge className={elementColors[element ?? ""] ?? "bg-gray-100 text-gray-800"}>{element}</Badge> </div> <p className="text-sm text-gray-500"> {element} {similar} type </p> </CardContent> </Card> ))} </div> )} </div> </div> ); }
import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { api } from "../api"; import { useFindMany } from "@gadgetinc/react"; import { AutoForm } from "@/components/auto"; const elementColors: Record<string, string> = { grass: "bg-green-100 text-green-800", fire: "bg-red-100 text-red-800", water: "bg-blue-100 text-blue-800", }; export default function () { const [{ data: gadgemon, error, fetching }] = useFindMany(api.gadgemon, { select: { id: true, name: true, sprite: { url: true }, element: true, similar: true, }, live: true, }); return ( <div className="container mx-auto px-4 py-2 space-y-6"> <div className="bg-white rounded-2xl border shadow-sm p-6"> <h2 className="text-2xl font-bold mb-4">Create a Gadgemon</h2> <AutoForm action={api.gadgemon.create} include={["name", "similar", "element"]} title="" /> </div> <div> <h2 className="text-2xl font-bold mb-6">Gadgedex</h2> {error && ( <Alert variant="destructive" className="mb-6"> <AlertDescription>Failed to load Gadgemon: {error.message}</AlertDescription> </Alert> )} {fetching && !gadgemon && <p className="text-gray-400">Loading...</p>} {!fetching && !error && gadgemon?.length === 0 && <p className="text-gray-400">No Gadgemon yet. Create one above!</p>} {gadgemon && gadgemon.length > 0 && ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {gadgemon.map(({ id, name, sprite, element, similar }) => ( <Card key={id} className="overflow-hidden hover:shadow-md transition-shadow"> <div className="aspect-square bg-gray-50 flex items-center justify-center text-4xl text-gray-300"> {sprite?.url ? <img src={sprite.url} alt={name || "Gadgemon"} className="w-full h-full object-cover" /> : "🎭"} </div> <CardContent className="p-4"> <div className="flex items-center justify-between mb-1"> <h3 className="font-semibold truncate">{name || "Unnamed Gadgemon"}</h3> <Badge className={elementColors[element ?? ""] ?? "bg-gray-100 text-gray-800"}>{element}</Badge> </div> <p className="text-sm text-gray-500"> {element} {similar} type </p> </CardContent> </Card> ))} </div> )} </div> </div> ); }
More on frontends

Like your database and backend, your Gadget frontends are already hosted and live on the internet. For more information on frontends, read the frontend docs.

Test your app 

Now you can view your completed app (and your Gadgémon!) in the frontend:

  1. Click Preview in the top right of the Gadget editor
  2. Sign up and sign in to your app

You should see your Gadgémon displayed on the page.

Try creating new Gadgémon using the form!

A screenshot of the completed app. There are 3 Gadgemon visible in the app frontend, Gadgetbot the grass robot, Bob the fire whale, and Kaypeedee the water Honda Odyssey minivan. The form with Name, Looks similar to a ... and element fields is present, but empty.

Next steps 

Dig deeper into the concepts used in this tutorial:

Learn about development practices and tooling:

Questions? 

Reach out on Gadget's Discord server to talk with Gadget employees and the Gadget developer community!

Was this page helpful?