Skip to main content

Creating New Resources

To create new resources in this project, you implement a dialog-based form using the AddItem component, which leverages react-hook-form for validation and tanstack-query to interact with the ItemsService backend client.

Implementing the AddItem Component

The AddItem component manages the creation lifecycle: opening a dialog, validating input, executing the API request, and refreshing the UI.

// frontend/src/components/Items/AddItem.tsx

import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"

import { type ItemCreate, ItemsService } from "@/client"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { LoadingButton } from "@/components/ui/loading-button"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"

const formSchema = z.object({
title: z.string().min(1, { message: "Title is required" }),
description: z.string().optional(),
})

type FormData = z.infer<typeof formSchema>

const AddItem = () => {
const [isOpen, setIsOpen] = useState(false)
const queryClient = useQueryClient()
const { showSuccessToast, showErrorToast } = useCustomToast()

const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: { title: "", description: "" },
})

const mutation = useMutation({
mutationFn: (data: ItemCreate) =>
ItemsService.createItem({ requestBody: data }),
onSuccess: () => {
showSuccessToast("Item created successfully")
form.reset()
setIsOpen(false)
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["items"] })
},
})

const onSubmit = (data: FormData) => {
mutation.mutate(data)
}

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="my-4">Add Item</Button>
</DialogTrigger>
<DialogContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input {...field} required />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<LoadingButton type="submit" loading={mutation.isPending}>
Save
</LoadingButton>
</form>
</Form>
</DialogContent>
</Dialog>
)
}

Key Implementation Details

1. API Integration via ItemsService

The component uses the ItemsService.createItem method from the generated SDK (frontend/src/client/sdk.gen.ts). This method performs a POST request to /api/v1/items/.

// frontend/src/client/sdk.gen.ts

public static createItem(data: ItemsCreateItemData): CancelablePromise<ItemsCreateItemResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/items/',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}

2. Form Validation with Zod

Validation is enforced both by the formSchema and the UI. The title field is mandatory, while description is optional. The zodResolver connects this schema to react-hook-form.

3. State Management and Cache Invalidation

  • isOpen: Manually controlled state to ensure the dialog only closes after a successful API response.
  • onSuccess: Resets the form and closes the dialog.
  • onSettled: Calls queryClient.invalidateQueries({ queryKey: ["items"] }). This is critical as it triggers a background refetch of the items list, ensuring the UI reflects the newly created resource immediately.

Usage in Layouts

The AddItem component is typically placed in a header or action bar. For example, in frontend/src/routes/_layout/items.tsx, it is used alongside the page title:

// frontend/src/routes/_layout/items.tsx

<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Items</h1>
<p className="text-muted-foreground">Create and manage your items</p>
</div>
<AddItem />
</div>

Troubleshooting

  • Validation Errors: If the "Save" button doesn't trigger the mutation, check the FormMessage components. The zodResolver will prevent submission if the title is empty.
  • Loading States: Use the LoadingButton component and pass mutation.isPending to the loading prop to disable the button and show a spinner during the request.
  • API Failures: The onError handler uses handleError.bind(showErrorToast) from frontend/src/utils/index.ts to automatically parse backend error messages and display them as toast notifications.