|
| 1 | +# Prompt to Create a New App |
| 2 | + |
| 3 | +> Use this prompt in an AI editor (Cursor, Windsurf) to create a new app |
| 4 | +
|
| 5 | +**Important:** Run this alongside information about the app you're creating, be |
| 6 | +it a documentation or OpenAPI specification. |
| 7 | + |
| 8 | +A deco app allows a service API to be exposed using Typescript functions. After |
| 9 | +a service it wrapped in an app, it can be used |
| 10 | + |
| 11 | +- As a data source in deco CMS |
| 12 | +- As a MCP server in deco.chat |
| 13 | + |
| 14 | +For the AI to create the necessary typings and functions, you need to provide a |
| 15 | +data source for the API documentation. It can be the plain text of an API docs |
| 16 | +or an OpenAPI specification. |
| 17 | + |
| 18 | +## Instructions |
| 19 | + |
| 20 | +A new app should be placed inside a folder on the root of this repo with the app |
| 21 | +name. For example, if we're creating an app for Figma, the app files will be on |
| 22 | +`figma/` |
| 23 | + |
| 24 | +### client.ts |
| 25 | + |
| 26 | +The client.ts is one of the central pieces of an app. It defines: |
| 27 | + |
| 28 | +- Typings for the data entities that the API accepts/returns. |
| 29 | +- All API methods with typed input and output |
| 30 | + |
| 31 | +The client interface follows a specific pattern that works with the |
| 32 | +`createHttpClient` utility. Here's a detailed example: |
| 33 | + |
| 34 | +```typescript |
| 35 | +// First, define your data types/interfaces |
| 36 | +export interface GithubUser { |
| 37 | + login: string; |
| 38 | + id: number; |
| 39 | + avatar_url: string; |
| 40 | +} |
| 41 | + |
| 42 | +// Then define your client interface |
| 43 | +// The key format must be: "HTTP_METHOD /path/to/endpoint" |
| 44 | +// Parameters in the URL path must be prefixed with : (e.g. :username) |
| 45 | +// Optional parameters must end with ? (e.g. :filter?) |
| 46 | +export interface GithubClient { |
| 47 | + // Simple GET request with URL parameters |
| 48 | + "GET /users/:username": { |
| 49 | + response: GithubUser; // Type of the response |
| 50 | + }; |
| 51 | + |
| 52 | + // POST request with body and URL parameters |
| 53 | + "POST /users/:username": { |
| 54 | + response: GithubUser; |
| 55 | + body: { |
| 56 | + filter: string; |
| 57 | + }; |
| 58 | + }; |
| 59 | + |
| 60 | + // GET request with query parameters (searchParams) |
| 61 | + "GET /search/users": { |
| 62 | + response: { users: GithubUser[] }; |
| 63 | + searchParams: { |
| 64 | + q: string; |
| 65 | + page?: number; |
| 66 | + per_page?: number; |
| 67 | + }; |
| 68 | + }; |
| 69 | + |
| 70 | + // POST with both URL params, query params and body |
| 71 | + "POST /repos/:owner/:repo/issues": { |
| 72 | + response: { id: number }; |
| 73 | + body: { |
| 74 | + title: string; |
| 75 | + body: string; |
| 76 | + }; |
| 77 | + searchParams: { |
| 78 | + assignee?: string; |
| 79 | + labels?: string[]; |
| 80 | + }; |
| 81 | + }; |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +Key points about the client interface: |
| 86 | + |
| 87 | +1. **HTTP Methods**: Must be one of: GET, PUT, POST, DELETE, PATCH, HEAD |
| 88 | + |
| 89 | +2. **URL Parameters**: |
| 90 | + - Required parameters: `:paramName` |
| 91 | + - Optional parameters: `:paramName?` |
| 92 | + - Wildcard parameters: `*` or `*paramName` |
| 93 | + |
| 94 | +3. **Response Type**: |
| 95 | + - Always defined in the `response` property |
| 96 | + - Can be any TypeScript type/interface |
| 97 | + - Optional if the endpoint doesn't return data |
| 98 | + |
| 99 | +4. **Request Body**: |
| 100 | + - Defined in the `body` property |
| 101 | + - Required for POST/PUT/PATCH methods |
| 102 | + - Must be a JSON-serializable object |
| 103 | + |
| 104 | +5. **Query Parameters**: |
| 105 | + - Defined in the `searchParams` property |
| 106 | + - All parameters are optional by default |
| 107 | + - Can be primitive types or arrays |
| 108 | + |
| 109 | +Example usage with the client: |
| 110 | + |
| 111 | +```typescript |
| 112 | +const api = createHttpClient<GithubClient>({ |
| 113 | + base: "https://api.github.com", |
| 114 | + headers: new Headers({ |
| 115 | + "Authorization": `Bearer ${token}`, |
| 116 | + }), |
| 117 | +}); |
| 118 | + |
| 119 | +// Using URL parameters |
| 120 | +const user = await api["GET /users/:username"]({ |
| 121 | + username: "octocat", |
| 122 | +}); |
| 123 | + |
| 124 | +// Using query parameters |
| 125 | +const search = await api["GET /search/users"]({ |
| 126 | + q: "john", |
| 127 | + page: 1, |
| 128 | + per_page: 10, |
| 129 | +}); |
| 130 | + |
| 131 | +// Using body and URL parameters |
| 132 | +const issue = await api["POST /repos/:owner/:repo/issues"]( |
| 133 | + { |
| 134 | + owner: "octocat", |
| 135 | + repo: "Hello-World", |
| 136 | + }, |
| 137 | + { |
| 138 | + body: { |
| 139 | + title: "Found a bug", |
| 140 | + body: "This is a bug report", |
| 141 | + }, |
| 142 | + }, |
| 143 | +); |
| 144 | +``` |
| 145 | + |
| 146 | +The client interface is used by the `createHttpClient` utility to: |
| 147 | + |
| 148 | +- Type-check URL parameters |
| 149 | +- Type-check request bodies |
| 150 | +- Type-check query parameters |
| 151 | +- Provide type-safe responses |
| 152 | +- Handle URL construction |
| 153 | +- Handle JSON serialization |
| 154 | +- Manage headers and authentication |
| 155 | + |
| 156 | +### mod.ts |
| 157 | + |
| 158 | +The mod.ts is the entrypoint for the app and it declares the **app |
| 159 | +configuration**, like API token or account name. This is information that is |
| 160 | +required for all methods in the API and it might be better if the user informs |
| 161 | +only once (when installing the app). |
| 162 | + |
| 163 | +It also instantiates the client or any other SDK/information that will be passed |
| 164 | +as context for every action and loader when executed. It uses the |
| 165 | + |
| 166 | +Example: |
| 167 | + |
| 168 | +```typescript |
| 169 | +import type { App, FnContext } from "@deco/deco"; |
| 170 | +import { fetchSafe } from "../utils/fetch.ts"; |
| 171 | +import { createHttpClient } from "../utils/http.ts"; |
| 172 | +import type { Secret } from "../website/loaders/secret.ts"; |
| 173 | +import manifest, { Manifest } from "./manifest.gen.ts"; |
| 174 | +import { ClientInterfaceExample } from "./client.ts"; |
| 175 | + |
| 176 | +export type AppContext = FnContext<State, Manifest>; |
| 177 | + |
| 178 | +export interface Props { |
| 179 | + /** |
| 180 | + * @title Account Name |
| 181 | + * @description erploja2 etc |
| 182 | + */ |
| 183 | + account: string; |
| 184 | + |
| 185 | + /** |
| 186 | + * @title API token |
| 187 | + * @description The token for accessing your API |
| 188 | + */ |
| 189 | + token?: string | Secret; |
| 190 | +} |
| 191 | + |
| 192 | +// Here we define the state of the app |
| 193 | +// You choose what to put in the state |
| 194 | +export interface State extends Omit<Props, "token"> { |
| 195 | + api: ReturnType<typeof createHttpClient<ClientInterfaceExample>>; |
| 196 | +} |
| 197 | + |
| 198 | +/** |
| 199 | + * @name App Template |
| 200 | + * @description This is an template of an app to be used as a reference. |
| 201 | + * @category Developer Tools |
| 202 | + * @logo https:// |
| 203 | + */ |
| 204 | +export default function App(props: Props): App<Manifest, State> { |
| 205 | + const { token, account: _account } = props; |
| 206 | + |
| 207 | + const _stringToken = typeof token === "string" ? token : token?.get?.() ?? ""; |
| 208 | + |
| 209 | + const api = createHttpClient<ClientInterfaceExample>({ |
| 210 | + base: `https://api.github.com/users/guitavano`, |
| 211 | + headers: new Headers({ "Authorization": `Bearer ${_stringToken}` }), |
| 212 | + fetcher: fetchSafe, |
| 213 | + }); |
| 214 | + |
| 215 | + // it is the state of the app, all data |
| 216 | + // here will be available in the context of |
| 217 | + // loaders, actions and workflows |
| 218 | + const state = { ...props, api }; |
| 219 | + |
| 220 | + return { |
| 221 | + state, |
| 222 | + manifest, |
| 223 | + }; |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +### Actions and Loaders |
| 228 | + |
| 229 | +An app is used, after installed, by calling its actions and loaders. |
| 230 | + |
| 231 | +Actions and Loaders are Typescript functions that abstract the API methods of |
| 232 | +the service being wrapped. |
| 233 | + |
| 234 | +Loaders are used to retrieve data. |
| 235 | + |
| 236 | +Actions are used when you save or update data in the external services. |
| 237 | + |
| 238 | +To declare an action or loader, it's just needed to create a `{actionName}.ts` |
| 239 | +inside `{appFolder}/actions/` or `{loaderName}.ts` inside |
| 240 | +`{appFolder}/loaders/`. You can use intermediary folders for organization. |
| 241 | + |
| 242 | +Examples: |
| 243 | + |
| 244 | +### Loader Example |
| 245 | + |
| 246 | +```typescript |
| 247 | +import { AppContext } from "../mod.ts"; |
| 248 | +import { GithubUser } from "../utils/types.ts"; |
| 249 | + |
| 250 | +interface Props { |
| 251 | + username: string; |
| 252 | +} |
| 253 | + |
| 254 | +/** |
| 255 | + * @title This name will appear on the admin |
| 256 | + */ |
| 257 | +const loader = async ( |
| 258 | + props: Props, |
| 259 | + _req: Request, |
| 260 | + ctx: AppContext, |
| 261 | +): Promise<GithubUser> => { |
| 262 | + const response = await ctx.api[`GET /users/:username`]({ |
| 263 | + username: props.username, |
| 264 | + }); |
| 265 | + |
| 266 | + const result = await response.json(); |
| 267 | + |
| 268 | + return result; |
| 269 | +}; |
| 270 | + |
| 271 | +export default loader; |
| 272 | +``` |
| 273 | + |
| 274 | +### Action Example |
| 275 | + |
| 276 | +```typescript |
| 277 | +import { AppContext } from "../mod.ts"; |
| 278 | +import { GithubUser } from "../utils/types.ts"; |
| 279 | + |
| 280 | +interface Props { |
| 281 | + username: string; |
| 282 | +} |
| 283 | + |
| 284 | +/** |
| 285 | + * @title This name will appear on the admin |
| 286 | + */ |
| 287 | +const action = async ( |
| 288 | + props: Props, |
| 289 | + _req: Request, |
| 290 | + ctx: AppContext, |
| 291 | +): Promise<GithubUser> => { |
| 292 | + const response = await ctx.api[`POST /users/:username`]({ |
| 293 | + username: props.username, |
| 294 | + }, { body: { filter: "filter" } }); |
| 295 | + |
| 296 | + const result = await response.json(); |
| 297 | + |
| 298 | + return result; |
| 299 | +}; |
| 300 | + |
| 301 | +export default action; |
| 302 | +``` |
| 303 | + |
| 304 | +### deco.ts |
| 305 | + |
| 306 | +In root `deco.ts`, add a new entry for the newly created app. |
| 307 | + |
| 308 | +``` |
| 309 | +const config = { |
| 310 | + apps: [ |
| 311 | + app("deno"), |
| 312 | + app("figma"), |
| 313 | + app("unsplash"), |
| 314 | + app("reflect"), |
| 315 | + app("grain"), |
| 316 | + app("slack"), |
| 317 | +``` |
| 318 | + |
| 319 | +### Manifest |
| 320 | + |
| 321 | +In every app folder there's also a `manifest.gen.ts` that exports all actions |
| 322 | +and loaders from an app. You don't need to worry about this file because it will |
| 323 | +be automatically generated when running `deno task start` in the root folder. |
| 324 | + |
| 325 | +Generate a first version so the app doesn't break: Example |
| 326 | + |
| 327 | +```typescript |
| 328 | +// DO NOT EDIT. This file is generated by deco. |
| 329 | +// This file SHOULD be checked into source version control. |
| 330 | +// This file is automatically updated during development when running `dev.ts`. |
| 331 | + |
| 332 | +import * as $$$$$$$$$0 from "./actions/myAction.ts"; |
| 333 | +import * as $$$0 from "./loaders/myLoader.ts"; |
| 334 | +import * as $$$$$$0 from "./sections/mySection.tsx"; |
| 335 | + |
| 336 | +const manifest = { |
| 337 | + "loaders": { |
| 338 | + "app-template/loaders/myLoader.ts": $$$0, |
| 339 | + }, |
| 340 | + "sections": { |
| 341 | + "app-template/sections/mySection.tsx": $$$$$$0, |
| 342 | + }, |
| 343 | + "actions": { |
| 344 | + "app-template/actions/myAction.ts": $$$$$$$$$0, |
| 345 | + }, |
| 346 | + "name": "app-template", |
| 347 | + "baseUrl": import.meta.url, |
| 348 | +}; |
| 349 | + |
| 350 | +export type Manifest = typeof manifest; |
| 351 | + |
| 352 | +export default manifest; |
| 353 | +``` |
| 354 | + |
| 355 | +## Manifest Gen |
| 356 | + |
| 357 | +In the end, run `deno task start` to regenerate the manifest. |
0 commit comments