TGViewer
Web Development Web Development @web_dev_bds · 4.43K subscribers
Post #1058 43
Strong opinion:
You shouldn't write Server Actions that take FormData as an argument. They should take normal typed args instead.


The whole point of Server Actions is that they let us use TypeScript functions to call server code from the client.

The reason we want to use typed functions instead of fetch() is exactly the same as why we want to use Prisma or Drizzle to query our databases: they give us feedback directly in our editor when we write incorrect code.

If we write
const sql = "INSERT INTO users (namee) VALUES ('sam')"


We won't know we've made a mistake until we execute the query. The SQL statement is actually a data structure, but that structure is hidden behind an opaque string. Tools like Prisma and Drizzle make that structure comprehensible by TypeScript, so that when we write
prisma.users.create({ data: { namee: 'sam' }})

we know right away we've made a typo, and can fix it without needing to run the code.

The same is true for Server Actions. If we write an action that takes formData:
"use server"
async function createUser(formData: FormData) {
const name = formData.get('name')
await prisma.users.create({ data: { name }})
}

then call it from the client
<form action={createUser}>
<input type='text' name='namee' />
</form>

we'll get zero feedback that our HTML has a typo, until we run the code. The HTML `name` attribute is an opaque string, and like the SQL example, it has obscured the data structure underneath.

Compare that to this version, that accepts a normal name prop typed as a string:
"use server"
async function createUser(name: string) {
await prisma.users.create({ data: { name }})
}

Now in the client, if we make a mistake:
const [name, setName] = useState('')

<form action={() => createUser(namee)}>
<input type='text'
value={name}
onChange={e => setName(e​.target.value)}
/>
</form>

we'll know right away, thanks to createUser using the specific type we expect, rather than taking an opaque FormData object.

Note that this is exactly how we normally write event handlers in React:
<button onClick={() => setCount(count + 1)} />

You wouldn't use an opaque data structure to pull this off...
<button onClick={() => exec("increment 'count' by 1"} />

...because you don't have to. You're working in JavaScript, and you get to use all the features and data structures the language gives you to help you write your code. API calls and DB queries cross a serialization boundary, so traditionally that's where we've had to leave the safety of the language.

But where tools like Prisma brought us back to the language to write our db queries, Server Functions have now done exactly the same thing for our API calls.

Using FormData effectively breaks us out of the language again, taking our data structures and type safety right along with it.
More from @web_dev_bds
  1. Oct 16, 2026📕Random React Tip: If you want to quickly log things from JSX (we've all been there), you…
  2. Oct 15, 2026Websites to Learn Every Technology
  3. Oct 13, 2026Docker vs Kubernetes
  4. Oct 12, 2026Post #1054
  5. Oct 11, 2026CSS Tip Stop centring tooltips with 𝚝𝚛𝚊𝚗𝚜𝚕𝚊𝚝𝚎: -𝟻𝟶%. near the edge of the scree…
  6. Oct 9, 2026JavaScript CheatSheet
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →