Quickstart: Build & deploy a full-stack AI app
Hello, and welcome to Gadget!
In this quickstart you will build an app that creates and displays custom Gadgémon (Gadget Monsters). Custom sprites for your Gadgémon will be generated using OpenAI's DALL-E API and Gadget's built-in OpenAI connection.
What you'll learn
After you have finished this quickstart, you will have accomplished the following:
- Built a custom data model
- Constructed an action using the OpenAI connection (and interacted with the CRUD API)
- Managed your app's permissions
- Setup your React frontend
- Deployed your application
Step 1: Create a new Gadget app
Every Gadget app includes a hosted Postgres database, a serverless Node backend, and a Vite + React frontend. You will use all of these to build an app!
- Start by creating a new Gadget app at https://gadget.new
- Select the AI app template and enter your app's domain name
- Click Get started

Step 2: Build your model
Models in Gadget are similar to tables in a database. They are the building blocks of your app. Models have one or more fields. You can think of fields as columns in a database table. You can create as many models and fields as you need to build your app.
You are going to create a model to store your Gadgémon data, which includes fields to store name, type, what the Gadgémon looks similar to, and an image.
- Click the + button in the Data Models section of the toolbar to create a new model
- Rename your model's API identifier to gadgemon
When you create a new model, you get some default fields for your model's records:
- id: stores the unique ids for records, or rows, in your model
- createdAt and updatedAt: timestamps that store the time when a record was created and last updated
You can also add custom fields to your models:
- Click the + button in the Fields section of the model page to create a new field
- Rename your field's API identifier name
This string field will be used to store your Gadgémon's name. Now you can add additional fields to your model:
- Add another new string field with an API identifier of similar
- Add a new field with an API identifier of type
- Change your type field's Type or Relationship to enum
- Use the + Add Option button to add 3 options to your enum: grass, fire, and water
- Add a new field with an API identifier of sprite
- Change your sprite field's Type or Relationship to file
These fields will be used to store "type" info as well as a sprite image file that will be generated for your Gadgémon.
Your model should now look like this:

That's it for building your Gadgémon model! Changes to your Gadget apps are automatically saved as you build, so you can move on to the next step.
Step 3: Add custom code to an action
Now that you have added fields to your Gadgémon model, you need to be able to create new records. Records in Gadget are just rows in a database table. To add new records, you'll need to call your app's API.
Good news! As you build models in Gadget, a CRUD (create, read, update, delete) API is automatically generated for you! This means that your Gadgemon model already has create, update, and delete actions that you can use to interact with your Gadgémon data (and a read action to fetch your Gadgémon data, but that will be covered later). You can see these actions and the code that powers them in the Actions section of the model page.
You will also want to run some custom code when you create a new Gadgémon. In this app, users will enter a name and "similar" string, and pick a type for their Gadgémon. When users click a button in the UI to create their Gadgémon, an HTTP request will be made that returns an OpenAI-generated sprite. You will use the create
action to run the custom code required to make this HTTP request.
The Gadget AI app template comes with an OpenAI connection set up with Gadget-managed credentials. This means you can test out the OpenAI API in your app without having to create an account or manage your own API keys. For more information, see our OpenAI connection docs.
- Click on the
create
action in the Actions section of thegadgemon
model page
A JavaScript file, gadgemon/create.js
is already created for you. Now you just need to add some custom code.
- Paste the following code into
gadgemon/create.js
(replace the entire file):
gadgemon/create.jsjs1import { applyParams, save, ActionOptions, CreateGadgemonActionContext } from "gadget-server";23/**4 * @param { CreateGadgemonActionContext } context5 */6export async function run({ params, record, logger, api }) {7 applyParams(params, record);8 await save(record);9}1011/**12 * @param { CreateGadgemonActionContext } context13 */14export async function onSuccess({ params, record, logger, api, connections }) {15 // "record" is the newly created Gadgemon, with name, similar and type fields that will be added by the user16 const { id, name, similar, type } = record;1718 // prompt sent to OpenAI to generate the Gadgemon sprite19 const prompt = `A pixel art style pokemon sprite named ${name} that looks similar to a ${similar} that is a ${type} type. Do not include any text, including the name, in the image`;2021 // call the OpenAI images generate (DALL-E) API: https://github.com/openai/openai-node/blob/v4/src/resources/images.ts22 const response = await connections.openai.images.generate({23 prompt,24 n: 1,25 size: "256x256",26 response_format: "url",27 });2829 const imageUrl = response.data[0].url;3031 // write to the Gadget Logs32 logger.info({ imageUrl }, `Generated image URL for Gadgemon id ${id}`);3334 // save the image file to the newly created Gadgémon record35 await api.gadgemon.update(id, {36 gadgemon: {37 sprite: {38 copyURL: imageUrl,39 },40 },41 });42}4344/** @type { ActionOptions } */45export const options = {46 actionType: "create",47};
This code block has two built-in functions:
- the
run
function is the first thing that executes when thecreate
action is called. It takes theparams
from the action call and applies them to therecord
that is being created. This is how thename
,similar
, andtype
fields are added to the record. The new record is then saved to the database. Therun
function is transactional by default, and has a 5 second timeout. If there are any failures in therun
function, the entire action will fail and the record will not be saved to the database. - the
onSuccess
function is called after therun
function, and is good to use for any secondary effects, like sending notifications or passing data between external systems.
The custom code added to onSuccess
does three things:
- it makes a request to the OpenAI API and returns a new image for your Gadgémon
- it writes a log message to the Gadget Logs
- it saves the image file to the newly created Gadgémon record
Because your initial Gadgemon record (ie. your name, similar, and type) is already saved to your app's database, you need to use the api
from the function's parameters to update this record with a generated sprite image. The api
object gives you full access to your Gadget app's API, so you can run any CRUD or custom actions, in this case, the update
action is run.
copyURL
is a special command used to store files in your Gadget database from a URL. See the storing files documentation to learn more about storing files in Gadget.
Test your action with the API Playground
Gadget comes with a built-in API Playground where you can test out your backend API. You can use the API Playground to test out your Gadgémon models' create action.
- On the Gadgémon model's page, click the
create
action, then the Run Action button - the API Playground will open with the create mutation pre-loaded - Enter some values for your Gadgémon's name, similar, and type fields in the Variables section of the Playground
json1{2 "gadgemon": {3 "name": "Gadgetbot",4 "similar": "robot",5 "type": "grass"6 }7}
- Click the run button to run the create action
This will create a new record in your model, and send off a request to generate a new sprite image! You should see a success message in the playground, and can hover over the sprite url field to preview your Gadgémon! You can also view your record on the model's Data page.
- Click on the
gadgemon
model in the sidebar - Click on the Data tab

You can see that your record was stored in the database and that an image was generated. You can view the generated image by copying the url value from the sprite field and pasting it into your browser.
To view the log message you wrote in your action, you can view the Gadget Logs:
- Click on Logs in the sidebar
- Turn the My events toggle on by clicking the switch - you may also need to adjust the time range to see your log message
You should see the log message that was written when you ran your action. You can make logger
calls in code effects at debug, info, or error levels, all of which are useful when debugging your app in both development and production environments.

Now that you know that you can create new Gadgémon records, you can move on to building a frontend so you can create and view your Gadgémon with a UI.
Step 4: Roles and permissions
Before we write frontend code, you need to make sure users can make requests to your API from the frontend!
By default, unauthenticated users are not granted access to your app's API. For this app, we won't worry about authentication, but we will need to grant access to the Gadgemon model's read and create actions for unauthenticated users so you can create and view your Gadgemon in the frontend.
- Click on Settings in the sidebar
- Click on Roles and Permissions
- Check the Gadgemon model's
read
andcreate
actions for theunauthenticated
role
Now, requests made to your backend will succeed! Move on to the next step: building your React frontend.
Step 5: Setup your frontend
In Gadget, your Vite + React frontend is found inside the frontend
folder. New Gadget apps have a starter template that you will edit to build your app frontend:
- a Vite project is initialized, including the
vite.config.js
andindex.html
files in the root folder of your app - a default React project is set up in the
frontend
folder, includingmain.jsx
,App.jsx
, andApp.css
, and two routes in thefrontend/routes
folder - your backend API client is already set up in
frontend/api.js
, and is imported intoApp.jsx
First, install a component library to style your frontend.
Install Material UI (MUI)
You may have noticed the package.json
in the file explorer. Gadget runs on Node.js, so you can install any Node.js package to your app. For this app, we will use Material UI (MUI) to style our frontend.
- Open the Gadget command palette with P or Ctrl P
- Type
>
to enable terminal mode - Run the following command:
Run in the Gadget command paletteyarnyarn add @mui/material @mui/styles @emotion/react @emotion/styled

This will install MUI and its dependencies to your app. Currently, only yarn
is supported for package management in your Gadget apps. Once the install is complete, you will be able to see these packages in your package.json
file.
Your React frontend will have two main sections: a form used to create new Gadgémon and a list of the Gadgémon that have already been created. Start by adding a section for displaying your already-created Gadgémon.
Display your Gadgémon
To display the Gadgémon that was already created, you need to fetch the data from your backend API. You can use the useFindMany
hook, provided by the @gadgetinc/react
package to fetch all of the Gadgémon records from your backend API. This hook takes in the API client, which you can import from the api.js
file in the frontend
folder.
- Edit the
frontend/App.jsx
file to add the following code:
frontend/App.jsxjsx1import "./App.css";2import { api } from "./api";3import { useFindMany } from "@gadgetinc/react";4import { Paper, Grid, Card, Stack, CircularProgress } from "@mui/material";56const App = () => {7 // use the useFindMany hook to read records from the Gadgemon model8 const [{ data: myGadgemon, fetching: fetchingGadgemon, error: readError }] = useFindMany(api.gadgemon);910 return (11 <Paper>12 <Grid container spacing={2} p={2}>13 <Grid item xs={10}>14 <Stack spacing={2}>15 <h2>My Gadgémon</h2>16 <Grid container>17 {fetchingGadgemon && <CircularProgress />}18 {myGadgemon?.map((gadgemon, i) => (19 <Grid item xs={3} key={`gadgemon_${i}`} px={1} pt={0} pb={2}>20 <Card variant="outlined">21 <Grid container direction="column" alignItems="center">22 <b>{gadgemon.name}</b>23 <img src={gadgemon.sprite?.url} />24 the "{gadgemon.type} {gadgemon.similar}" Gadgémon25 </Grid>26 </Card>27 </Grid>28 ))}29 {myGadgemon?.length === 0 && <b>Start by creating a Gadgémon!</b>}30 </Grid>31 </Stack>32 </Grid>33 </Grid>34 </Paper>35 );36};3738export default App;
The key line to pay attention to is:
frontend/App.jsxjsconst [{ data: myGadgemon, fetching: fetchingGadgemon, error: readError }] = useFindMany(api.gadgemon);
The useFindMany
hook will read data from your gadgemon model, and return an object that includes:
- the returned
data
, aliased asmyGadgemon
fetching
, a boolean that is true while the request is running and aliased asfetchingGadgemon
error
, which will contain any errors that occur while fetching the data
You can use the objects returned from the hook to display the returned data, a loading message, or an error message to users. In the return
block of the provided code snippet, you can see how you are using myGadgemon
to display your monsters and fetchingGadgemon
to handle a loading spinner.
- Replace the contents of
frontend/App.css
with the following:
frontend/App.csscss1body {2 background: #f3f3f3;3 color: #252525;4 padding: 55px;5 line-height: 1.5;6 font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;7}
Now you can see your Gadgémon in the frontend! Just open up your development environment URL in your browser:
- Click on your app domain name at the top of the left sidebar
- Hover over Go to app
- Click on Development to open your app in a new tab
And you should see your Gadgémon displayed on the page!

Create new Gadgémon
Now add a form to create a new Gadgémon. You can use the useAction
hook, provided by the @gadgetinc/react
package to create a new Gadgémon record in your backend API. This hook takes in your Gadget API's gadgemon.create
action, which you can import from api.js
.
Here is the complete code for frontend/App.jsx
file. Step-by-step instructions will follow:
frontend/App.jsxjsx1import { useState } from "react";2import "./App.css";3import { api } from "./api";4import { useAction, useFindMany } from "@gadgetinc/react";5import { Paper, Grid, Card, Stack, CircularProgress, TextField, MenuItem, FormControl, Button, LinearProgress, Alert } from "@mui/material";67const App = () => {8 // set up React state to handle form inputs9 const [name, setName] = useState("");10 const [similar, setSimilar] = useState("");11 const [type, setType] = useState("grass");1213 // the useAction hook is used to call the Gadgemon create action14 const [{ data: newGadgemon, fetching: creatingGadgemon, error: createError }, createGadgemon] = useAction(api.gadgemon.create);15 // the useFindMany hook is used to read records from the Gadgemon model16 const [{ data: myGadgemon, fetching: fetchingGadgemon }] = useFindMany(api.gadgemon);1718 // the createGadgetmon function defined with the useAction hook is called when the form is submitted19 const onSubmit = async (event) => {20 event.preventDefault();21 await createGadgemon({22 gadgemon: {23 name,24 similar,25 type,26 },27 });28 };2930 return (31 <Paper>32 {createError && (33 <Alert severity="error">34 <code>{createError.message}</code>35 </Alert>36 )}37 <Grid container spacing={2} p={2}>38 <Grid item xs={2}>39 <FormControl fullWidth>40 <form onSubmit={onSubmit}>41 <Stack spacing={2}>42 <h2>Gadgémon factory</h2>43 <TextField44 label="Name"45 size="small"46 fullWidth47 variant="outlined"48 value={name}49 onChange={(event) => setName(event.target.value)}50 required51 disabled={creatingGadgemon}52 />53 <TextField54 label="Looks like a..."55 size="small"56 fullWidth57 variant="outlined"58 value={similar}59 onChange={(event) => setSimilar(event.target.value)}60 required61 disabled={creatingGadgemon}62 />63 <TextField value={type} onChange={(e) => setType(e.target.value)} select label="Type" required disabled={creatingGadgemon}>64 <MenuItem value={"grass"}>Grass</MenuItem>65 <MenuItem value={"water"}>Water</MenuItem>66 <MenuItem value={"fire"}>Fire</MenuItem>67 </TextField>6869 <Button variant="contained" disabled={creatingGadgemon} type="submit">70 Create Gadgémon71 </Button>7273 {creatingGadgemon && (74 <Stack spacing={1}>75 <span>Creating new Gadgémon!</span>76 <LinearProgress />77 </Stack>78 )}79 {newGadgemon?.sprite && (80 <Card variant="outlined">81 <img src={newGadgemon.sprite.url} />82 </Card>83 )}84 </Stack>85 </form>86 </FormControl>87 </Grid>88 <Grid item xs={10}>89 <Stack spacing={2}>90 <h2>My Gadgémon</h2>91 <Grid container>92 {fetchingGadgemon && <CircularProgress />}93 {myGadgemon?.map((gadgemon, i) => (94 <Grid item xs={3} key={`gadgemon_${i}`} px={1} pt={0} pb={2}>95 <Card variant="outlined">96 <Grid container direction="column" alignItems="center">97 <b>{gadgemon.name}</b>98 <img src={gadgemon.sprite?.url} />99 the "{gadgemon.type} {gadgemon.similar}" Gadgémon100 </Grid>101 </Card>102 </Grid>103 ))}104 {myGadgemon?.length === 0 && <b>Start by creating a Gadgémon!</b>}105 </Grid>106 </Stack>107 </Grid>108 </Grid>109 </Paper>110 );111};112113export default App;
Step-by-step instructions
- Import the
useAction
hook from@gadgetinc/react
(in addition to the already-useduseFindMany
hook):
frontend/App.jsxjsimport { useAction, useFindMany } from "@gadgetinc/react";
- Import
useState
fromreact
:
frontend/App.jsxjsimport React, { useState } from "react";
- Set up React state for the form inputs:
frontend/App.jsxjs1// imports23const App = () => {4 const [name, setName] = useState("");5 const [similar, setSimilar] = useState("");6 const [type, setType] = useState("grass");78 // your hook to read data from your app API910 return (/** return components */);11}
- Make use of the
useAction
hook to call thegadgemon.create
action when the form is submitted:
frontend/App.jsxjs1// imports23const App = () => {4 // React state and useFindMany hook56 // the useAction hook is used to call the gadgemon.create action7 // the created data, a fetching boolean, and error state is returned8 // along with a function callback `createGadgemon` that can be used to call the action9 const [{ data: newGadgemon, fetching: creatingGadgemon, error: createError }, createGadgemon] = useAction(api.gadgemon.create);1011 // an onSubmit function that calls `createGadgemon` when the form is submitted12 const onSubmit = async (event) => {13 event.preventDefault();1415 // make the request to your Gadget backend!16 await createGadgemon({17 gadgemon: {18 name,19 similar,20 type,21 },22 });23 };2425 return (/** return components */);26}
- Display an error message to users if the
createGadgemon
action fails:
frontend/App.jsxjsx1return (2 <Paper>3 {createError && (4 <Alert severity="error">5 <code>{createError.message}</code>6 </Alert>7 )}8 {/** components for displaying Gadgemon are here! */}9 </Paper>10);
- Add a form to the page that calls the
onSubmit
function when submitted:
frontend/App.jsxjsx1return (2 <Paper>3 <Grid container spacing={2} p={2}>4 {/** place the below component code under the first Grid container */}5 <Grid item xs={2}>6 <FormControl fullWidth>7 <form onSubmit={onSubmit}>8 <Stack spacing={2}>9 <h2>Gadgémon factory</h2>10 <TextField11 label="Name"12 size="small"13 fullWidth14 value={name}15 onChange={(event) => setName(event.target.value)}16 required17 disabled={creatingGadgemon}18 />19 <TextField20 label="Looks like a..."21 size="small"22 fullWidth23 value={similar}24 onChange={(event) => setSimilar(event.target.value)}25 required26 disabled={creatingGadgemon}27 />28 <TextField29 value={type}30 onChange={(e) => setType(e.target.value)}31 select32 label="Type"33 size="small"34 required35 disabled={creatingGadgemon}36 >37 <MenuItem value={"grass"}>Grass</MenuItem>38 <MenuItem value={"water"}>Water</MenuItem>39 <MenuItem value={"fire"}>Fire</MenuItem>40 </TextField>4142 <Button variant="contained" disabled={creatingGadgemon} type="submit">43 Create Gadgémon44 </Button>4546 {creatingGadgemon && (47 <Stack spacing={1}>48 <span>Creating new Gadgémon!</span>49 <LinearProgress />50 </Stack>51 )}52 {newGadgemon?.sprite && (53 <Card variant="outlined">54 <img src={newGadgemon.sprite.url} />55 </Card>56 )}57 </Stack>58 </form>59 </FormControl>60 </Grid>61 {/** components for displaying Gadgemon are here! */}62 </Grid>63 </Paper>64);
Test your app!
Head back to your development URL and you should see your form! Gadget frontends include hot module reloading so the frontend will update as you write code. Try creating a new Gadgémon.

Step 6: Deploy your app
Deploying to production is a two-click process:
- Click the Deploy button in the bottom right of the Gadget UI
- Then click the Deploy Changes button

Your app will be built and optimized for production, and available at your production URL! To view your app in production:
- Click on your app domain in the top left of the Gadget UI
- Hover over Go to app
- Click Production
Your production app will open, and you can create your first Gadgémon in production!
Congrats! You have built your first fullstack Gadget application and have learned how to:
- build data models
- add custom code to model actions
- change API permissions
- call your app's API (actions) from the frontend
Next steps
Want to expand your app? Here are some ideas:
- allow users to delete Gadgémon
- use OpenAI to generate a description for your Gadgémon
- send two Gadgemon into battle (using OpenAI) and see who wins
Shopify app development
Interested in using Gadget to build Shopify apps? Try the product tagger tutorial:
Build an embedded Shopify application that automatically tags products in a Shopify store based on description keywords.
AI app development
You can learn about some of Gadget's AI features such as vector embeddings and similarity searches with the AI screenwriter tutorial:
Learn how to build an AI chatbot that writes fake movie scenes using Gadget, OpenAI, and Vercel's AI SDK.
Questions?
Reach out on our Discord server to talk with Gadget employees and the Gadget developer community!