Posting FormData


server/api/user/_userId/index.ts
export type Methods = {
post: {
reqFormat: FormData // required
reqBody: { icon: Blob }
status: 204 // optional
}
}
server/api/user/_userId/controller.ts
import { defineController } from './$relay'
import { changeIcon } from '$/service/user'
export default defineController(() => ({
post: async ({ params, body }) => {
await changeIcon(params.userId, body.icon)
return { status: 204 }
}
}))
server/service/user.ts
import fs from 'fs'
import path from 'path'
import { Multipart } from 'fastify-multipart'
export const changeIcon = async (id: string, iconFile: Multipart) => {
const iconName = `${Date.now()}${path.extname(iconFile.filename)}`
await fs.promises.writeFile(
path.resolve('public/icons', id, iconName),
await iconFile.toBuffer()
)
}

fastify-multipart

server/service/app.ts
import Fastify from 'fastify'
import server from './$server' // '$server.ts' is automatically generated by frourio
const fastify = Fastify()
server(fastify, { basePath: '/api/v1', multipart: { fileSize: 1024 ** 3 } })
fastify.listen(3000)

Multipart options

Use aspida for the frontend HTTP client.
(Frourio and aspida are maintained by the same developer)

components/UserBanner.tsx
import { useCallback, ChangeEvent } from 'react'
import { apiClient } from '~/utils/apiClient'
const UserBanner = () => {
const editIcon = useCallback(
async (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files?.length) return
await apiClient.user._userId(1).$post({
body: { icon: e.target.files[0] }
})
},
[]
)
return <input type="file" accept="image/*" onChange={editIcon} />
}
export default UserBanner