Quickstart: Build & deploy a full-stack web app
Estimated time: 10 minutes
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 finishing this quickstart, you will know how to:
- Add a custom data model
- Construct an action using the OpenAI connection (and interact with the CRUD API)
- Build a React frontend
Step 1: Create a new Gadget app
Every Gadget app includes a hosted Postgres database, a serverless Node backend, and a Vite + React frontend.
- Start by creating a new Gadget app at https://gadget.new
- Select the Web app type, enter your app's domain name, and click Get started
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:
- Click the + button next to
api/models
to create a new model - Name your model
gadgemon
and hit Enter on your keyboard
You can add fields to your model on the schema page at api/models/gadgemon/schema
:
- Click the + button next to FIELDS and call the new field
name
(leave it as the default string type) - Now add the additional fields to your Gadgémon model:
similar
: a string field that describes what your Gadgémon looks likeelement
: an enum field with optionsgrass
,fire
, andwater
sprite
: a file field that will store a generated image
That's it for building your Gadgémon model! 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:
- Click on Settings in the sidebar
- Click on Plugins
- Select OpenAI from the list of plugins
- Leave the default Gadget development keys selected and click Add connection
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:
- Go to
api/model/gadgemon/actions/create.js
- Paste the following code (replace the entire file):
1import { applyParams, save, ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, record, logger, api }) => {4 applyParams(params, record);5 await save(record);6};78export const onSuccess: ActionOnSuccess = async ({9 params,10 record,11 logger,12 api,13 connections,14}) => {15 // "record" is the newly created Gadgemon, with name, similar and element fields that will be added by the user16 const { id, name, similar, element } = 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 ${element} element. 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};4344export const options: ActionOptions = {45 actionType: "create",46};
1import { applyParams, save, ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, record, logger, api }) => {4 applyParams(params, record);5 await save(record);6};78export const onSuccess: ActionOnSuccess = async ({9 params,10 record,11 logger,12 api,13 connections,14}) => {15 // "record" is the newly created Gadgemon, with name, similar and element fields that will be added by the user16 const { id, name, similar, element } = 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 ${element} element. 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};4344export const options: ActionOptions = {45 actionType: "create",46};
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:
- While in
api/models/gadgemon/actions/create.js
, click the Run Action button. - Use the playground's API client to create a Gadgémon:
await api.gadgemon.create({name: "Gadgetbot",similar: "robot",element: "grass",});
await api.gadgemon.create({name: "Gadgetbot",similar: "robot",element: "grass",});
- 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.
- Go to
api/models/gadgemon/data
to view your model records
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.
- Go to
web/routes/signed-in.jsx
and replace the contents with the following code:
1import { api } from "../api";2import { useFindMany, useActionForm } from "@gadgetinc/react";34export default function () {5 // the useActionForm hook is used to call the gadgemon.create action6 // and manages form state and submission7 const { submit, register, formState, error, reset } = useActionForm(api.gadgemon.create, {8 defaultValues: {9 name: "",10 similar: "",11 element: "",12 },13 onSuccess: () => {14 // reset the form once submission is complete15 reset();16 },17 });1819 // the useFindMany hook is used to read records from the Gadgemon model20 const [{ data: myGadgemon, fetching: fetchingGadgemon }] = useFindMany(api.gadgemon);2122 return (23 <>24 {error && (25 <p className="format-message error">26 <code>{error.message}</code>27 </p>28 )}29 <div30 style={{31 width: "80vw",32 height: "80vh",33 overflowY: "auto",34 display: "flex",35 flexDirection: "row",36 flexWrap: "wrap",37 gap: "32px",38 backgroundColor: "white",39 padding: "32px",40 boxShadow: "0 4px 4px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)",41 }}42 >43 <section style={{ width: "250px" }}>44 <h2>Gadgémon factory</h2>45 <form onSubmit={submit} className="custom-form" style={{ marginTop: "24px", alignItems: "center" }}>46 <input47 className="custom-input"48 type="text"49 placeholder="Name"50 disabled={formState.isSubmitting}51 {...register("gadgemon.name")}52 />53 <input54 className="custom-input"55 type="text"56 placeholder="Looks similar to a ..."57 disabled={formState.isSubmitting}58 {...register("gadgemon.similar")}59 />60 <select className="custom-input" disabled={formState.isSubmitting} {...register("gadgemon.element")}>61 <option value="grass">Grass</option>62 <option value="water">Water</option>63 <option value="fire">Fire</option>64 </select>65 <button type="submit" disabled={formState.isSubmitting}>66 Create Gadgémon67 </button>68 {formState.isSubmitting && <p>Creating Gadgémon...</p>}69 </form>70 </section>71 <section style={{ flexGrow: 1, maxWidth: "75%", margin: "0 auto" }}>72 <h2>Gadgémon gallery</h2>73 {fetchingGadgemon && <p className="format-message">Fetching Gadgémon...</p>}74 {/** iterate over gadgemon returned from useFindMany hook */}75 {!myGadgemon || myGadgemon.length == 0 ? (76 <p className="format-message">Start by creating a Gadgémon!</p>77 ) : (78 <div79 style={{80 display: "flex",81 flexDirection: "row",82 flexWrap: "wrap",83 width: "100%",84 marginTop: "16px",85 justifyContent: "center",86 }}87 >88 {myGadgemon?.map((gadgemon, i) => (89 <div90 key={`gadgemon_${i}`}91 style={{92 width: "256px",93 display: "flex",94 flexDirection: "column",95 alignItems: "center",96 padding: "4px",97 border: "1px solid lightgrey",98 margin: "8px",99 }}100 >101 <b>{gadgemon.name}</b>102 <img src={gadgemon.sprite?.url} />103 <p style={{ maxWidth: "80%" }}>104 the "{gadgemon.element} {gadgemon.similar}" Gadgémon105 </p>106 </div>107 ))}108 </div>109 )}110 </section>111 </div>112 </>113 );114}
1import { api } from "../api";2import { useFindMany, useActionForm } from "@gadgetinc/react";34export default function () {5 // the useActionForm hook is used to call the gadgemon.create action6 // and manages form state and submission7 const { submit, register, formState, error, reset } = useActionForm(api.gadgemon.create, {8 defaultValues: {9 name: "",10 similar: "",11 element: "",12 },13 onSuccess: () => {14 // reset the form once submission is complete15 reset();16 },17 });1819 // the useFindMany hook is used to read records from the Gadgemon model20 const [{ data: myGadgemon, fetching: fetchingGadgemon }] = useFindMany(api.gadgemon);2122 return (23 <>24 {error && (25 <p className="format-message error">26 <code>{error.message}</code>27 </p>28 )}29 <div30 style={{31 width: "80vw",32 height: "80vh",33 overflowY: "auto",34 display: "flex",35 flexDirection: "row",36 flexWrap: "wrap",37 gap: "32px",38 backgroundColor: "white",39 padding: "32px",40 boxShadow: "0 4px 4px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)",41 }}42 >43 <section style={{ width: "250px" }}>44 <h2>Gadgémon factory</h2>45 <form onSubmit={submit} className="custom-form" style={{ marginTop: "24px", alignItems: "center" }}>46 <input47 className="custom-input"48 type="text"49 placeholder="Name"50 disabled={formState.isSubmitting}51 {...register("gadgemon.name")}52 />53 <input54 className="custom-input"55 type="text"56 placeholder="Looks similar to a ..."57 disabled={formState.isSubmitting}58 {...register("gadgemon.similar")}59 />60 <select className="custom-input" disabled={formState.isSubmitting} {...register("gadgemon.element")}>61 <option value="grass">Grass</option>62 <option value="water">Water</option>63 <option value="fire">Fire</option>64 </select>65 <button type="submit" disabled={formState.isSubmitting}>66 Create Gadgémon67 </button>68 {formState.isSubmitting && <p>Creating Gadgémon...</p>}69 </form>70 </section>71 <section style={{ flexGrow: 1, maxWidth: "75%", margin: "0 auto" }}>72 <h2>Gadgémon gallery</h2>73 {fetchingGadgemon && <p className="format-message">Fetching Gadgémon...</p>}74 {/** iterate over gadgemon returned from useFindMany hook */}75 {!myGadgemon || myGadgemon.length == 0 ? (76 <p className="format-message">Start by creating a Gadgémon!</p>77 ) : (78 <div79 style={{80 display: "flex",81 flexDirection: "row",82 flexWrap: "wrap",83 width: "100%",84 marginTop: "16px",85 justifyContent: "center",86 }}87 >88 {myGadgemon?.map((gadgemon, i) => (89 <div90 key={`gadgemon_${i}`}91 style={{92 width: "256px",93 display: "flex",94 flexDirection: "column",95 alignItems: "center",96 padding: "4px",97 border: "1px solid lightgrey",98 margin: "8px",99 }}100 >101 <b>{gadgemon.name}</b>102 <img src={gadgemon.sprite?.url} />103 <p style={{ maxWidth: "80%" }}>104 the "{gadgemon.element} {gadgemon.similar}" Gadgémon105 </p>106 </div>107 ))}108 </div>109 )}110 </section>111 </div>112 </>113 );114}
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:
- Click Preview in the top right of the Gadget editor
- 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!
Next steps
Try out some of our other tutorials to learn more about building with Gadget:
Automated product tagger
Build an embedded Shopify app that automatically tags products using keywords from the product description.
Start building →
AI Screenwriter
Learn how to build a web app that writes fake movie scenes using the OpenAI plugin and vector embeddings.
Start building →
Questions?
Reach out on Gadget's Discord server to talk with Gadget employees and the Gadget developer community!