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:

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, 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:

  1. Click the + button next to api/models to create a new model
  2. 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:

  1. Click the + button next to FIELDS and call the new field name (leave it as the default string type)
  2. Now add the additional fields to your Gadgémon model:
  • similar: a string field that describes what your Gadgémon looks like
  • element: an enum field with options grass, fire, and water
  • sprite: a file field that will store a generated image
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

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:

  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
1import { applyParams, save, ActionOptions } from "gadget-server";
2
3export const run: ActionRun = async ({ params, record, logger, api }) => {
4 applyParams(params, record);
5 await save(record);
6};
7
8export 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 user
16 const { id, name, similar, element } = record;
17
18 // prompt sent to OpenAI to generate the Gadgemon sprite
19 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`;
20
21 // call the OpenAI images generate (DALL-E) API: https://github.com/openai/openai-node/blob/v4/src/resources/images.ts
22 const response = await connections.openai.images.generate({
23 prompt,
24 n: 1,
25 size: "256x256",
26 response_format: "url",
27 });
28
29 const imageUrl = response.data[0].url;
30
31 // write to the Gadget Logs
32 logger.info({ imageUrl }, `Generated image URL for Gadgemon id ${id}`);
33
34 // save the image file to the newly created Gadgémon record
35 await api.gadgemon.update(id, {
36 gadgemon: {
37 sprite: {
38 copyURL: imageUrl,
39 },
40 },
41 });
42};
43
44export const options: ActionOptions = {
45 actionType: "create",
46};
1import { applyParams, save, ActionOptions } from "gadget-server";
2
3export const run: ActionRun = async ({ params, record, logger, api }) => {
4 applyParams(params, record);
5 await save(record);
6};
7
8export 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 user
16 const { id, name, similar, element } = record;
17
18 // prompt sent to OpenAI to generate the Gadgemon sprite
19 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`;
20
21 // call the OpenAI images generate (DALL-E) API: https://github.com/openai/openai-node/blob/v4/src/resources/images.ts
22 const response = await connections.openai.images.generate({
23 prompt,
24 n: 1,
25 size: "256x256",
26 response_format: "url",
27 });
28
29 const imageUrl = response.data[0].url;
30
31 // write to the Gadget Logs
32 logger.info({ imageUrl }, `Generated image URL for Gadgemon id ${id}`);
33
34 // save the image file to the newly created Gadgémon record
35 await api.gadgemon.update(id, {
36 gadgemon: {
37 sprite: {
38 copyURL: imageUrl,
39 },
40 },
41 });
42};
43
44export 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:

  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",
});
await api.gadgemon.create({
name: "Gadgetbot",
similar: "robot",
element: "grass",
});
  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/signed-in.jsx and replace the contents with the following code:
web/routes/signed-in.jsx
React
1import { api } from "../api";
2import { useFindMany, useActionForm } from "@gadgetinc/react";
3
4export default function () {
5 // the useActionForm hook is used to call the gadgemon.create action
6 // and manages form state and submission
7 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 complete
15 reset();
16 },
17 });
18
19 // the useFindMany hook is used to read records from the Gadgemon model
20 const [{ data: myGadgemon, fetching: fetchingGadgemon }] = useFindMany(api.gadgemon);
21
22 return (
23 <>
24 {error && (
25 <p className="format-message error">
26 <code>{error.message}</code>
27 </p>
28 )}
29 <div
30 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 <input
47 className="custom-input"
48 type="text"
49 placeholder="Name"
50 disabled={formState.isSubmitting}
51 {...register("gadgemon.name")}
52 />
53 <input
54 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émon
67 </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 <div
79 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 <div
90 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émon
105 </p>
106 </div>
107 ))}
108 </div>
109 )}
110 </section>
111 </div>
112 </>
113 );
114}
1import { api } from "../api";
2import { useFindMany, useActionForm } from "@gadgetinc/react";
3
4export default function () {
5 // the useActionForm hook is used to call the gadgemon.create action
6 // and manages form state and submission
7 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 complete
15 reset();
16 },
17 });
18
19 // the useFindMany hook is used to read records from the Gadgemon model
20 const [{ data: myGadgemon, fetching: fetchingGadgemon }] = useFindMany(api.gadgemon);
21
22 return (
23 <>
24 {error && (
25 <p className="format-message error">
26 <code>{error.message}</code>
27 </p>
28 )}
29 <div
30 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 <input
47 className="custom-input"
48 type="text"
49 placeholder="Name"
50 disabled={formState.isSubmitting}
51 {...register("gadgemon.name")}
52 />
53 <input
54 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émon
67 </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 <div
79 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 <div
90 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émon
105 </p>
106 </div>
107 ))}
108 </div>
109 )}
110 </section>
111 </div>
112 </>
113 );
114}
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 

Try out some of our other tutorials to learn more about building with Gadget:

Questions? 

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

Was this page helpful?