Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Forms: sending data to the server

To add a post, a page has to send the form data to the API. Form data is sent on a user action, so you need $fetch with the POST method, not useFetch. The server is already prepared: the handler server/api/posts.post.ts from the server chapter.

The new post page

npx nuxi add page posts/new

Replace the content of app/pages/posts/new.vue:

<!-- app/pages/posts/new.vue -->
<script setup lang="ts">
const form = reactive({ title: '', text: '' })
 
async function submit() {
  const post = await $fetch('/api/posts', {
    method: 'POST',
    body: form
  })
  await navigateTo(`/posts/${post.id}`)
}
</script>
 
<template>
  <form @submit.prevent="submit">
    <h2>New post</h2>
    <input v-model="form.title" placeholder="Title">
    <textarea v-model="form.text" placeholder="Text" />
    <button>Publish</button>
  </form>
</template>

$fetch sends the form object in the request body as JSON. The server returns the created post, and navigateTo opens its page. Fill in the form at /posts/new and press "Publish": the new post opens. Add a <NuxtLink to="/posts/new"> link to the layout menu to reach the form.

Errors and the sending state

The server answers with 400 when a field is empty. In that case $fetch throws an error, and the response body is in its data field:

<!-- app/pages/posts/new.vue -->
<script setup lang="ts">
const form = reactive({ title: '', text: '' })
const errorMessage = ref('')
const sending = ref(false)
 
async function submit() {
  sending.value = true
  errorMessage.value = ''
  try {
    const post = await $fetch('/api/posts', { method: 'POST', body: form })
    await navigateTo(`/posts/${post.id}`)
  } catch (e) {
    const err = e as { data?: { message?: string } }
    errorMessage.value = err.data?.message ?? 'Could not save the post'
  } finally {
    sending.value = false
  }
}
</script>
 
<template>
  <form @submit.prevent="submit">
    <input v-model="form.title" placeholder="Title">
    <textarea v-model="form.text" placeholder="Text" />
    <p v-if="errorMessage">{{ errorMessage }}</p>
    <button :disabled="sending">Publish</button>
  </form>
</template>

Send the form with an empty text and the server message "Fill in the title and the text" appears below the fields. While the request runs, the button is disabled, so the post is not sent twice.

The comment form

Create the component app/components/post/CommentForm.vue:

<!-- app/components/post/CommentForm.vue -->
<script setup lang="ts">
const props = defineProps<{ postId: number }>()
const emit = defineEmits<{ added: [] }>()
 
const author = ref('')
const text = ref('')
 
async function send() {
  await $fetch(`/api/posts/${props.postId}/comments`, {
    method: 'POST',
    body: { author: author.value, text: text.value }
  })
  text.value = ''
  emit('added')
}
</script>
 
<template>
  <form @submit.prevent="send">
    <input v-model="author" placeholder="Your name">
    <textarea v-model="text" placeholder="Comment" />
    <button>Send</button>
  </form>
</template>

After sending, the component tells its parent with the added event. On the post page, where useAsyncData loads the data, add the form and the refresh:

<!-- app/pages/posts/[id].vue -->
<PostCommentForm v-if="data" :post-id="data.post.id" @added="refresh()" />

Take refresh from the result: const { data, refresh } = await useAsyncData(...). A new comment appears under the post right after sending.

Common mistake

Calling useFetch inside the submit handler. Composables like useFetch are meant for <script setup>: inside an event handler they behave unreliably and print a warning. For requests on a button click or form submit, use $fetch.

Thanks for reading!

Donate to the site — we work hard for you.