Skip to content

Tutorial: Use RESTful Forms

In this tutorial you will create HTML forms that submit DELETE and PUT requests using the _method hidden field, set up the middleware, and build a reusable pattern for RESTful forms.

Prerequisites

Step 1: Set Up the Middleware

The methodOverride middleware needs formData to come first, since it reads from the parsed form data:

ts
import { createRouter } from 'remix/fetch-router'
import { formData, FormData } from 'remix/form-data-middleware'
import { methodOverride } from 'remix/method-override-middleware'
import { route } from 'remix/fetch-router/routes'

let routes = route({
  posts: {
    index: '/posts',
    show: '/posts/:id',
  },
})

let router = createRouter({
  middleware: [
    formData(),
    methodOverride(),
  ],
})

Step 2: Create a Delete Form

Add a delete button to a post page. The form uses method="post" with a hidden _method="DELETE" field:

ts
router.get(routes.posts.show, async ({ params }) => {
  let post = await db.find(posts, params.id)

  return html`
    <h1>${post.title}</h1>
    <p>${post.body}</p>

    <form method="post" action="/posts/${post.id}">
      <input type="hidden" name="_method" value="DELETE" />
      <button type="submit">Delete this post</button>
    </form>
  `
})

The browser sends a POST. The middleware reads _method=DELETE, changes the request method to DELETE, and removes the _method field from the form data.

Step 3: Handle the DELETE Request

Write a handler for DELETE requests on the post route:

ts
router.delete(routes.posts.show, async ({ params }) => {
  await db.delete(posts, { where: eq(posts.columns.id, params.id) })
  return redirect('/posts')
})

This handler only runs for DELETE requests. The middleware ensures that forms with _method="DELETE" are routed here.

Step 4: Create an Update Form with PUT

Build an edit form that sends a PUT request:

ts
router.get(routes.posts.show, async ({ params }) => {
  let post = await db.find(posts, params.id)

  return html`
    <h1>Edit Post</h1>

    <form method="post" action="/posts/${post.id}">
      <input type="hidden" name="_method" value="PUT" />

      <label for="title">Title</label>
      <input type="text" id="title" name="title" value="${post.title}" />

      <label for="body">Body</label>
      <textarea id="body" name="body">${post.body}</textarea>

      <button type="submit">Update Post</button>
    </form>
  `
})

Step 5: Handle the PUT Request

ts
router.put(routes.posts.show, async ({ params, context }) => {
  let data = context.get(FormData)
  let title = data.get('title')
  let body = data.get('body')

  await db.update(posts, {
    where: eq(posts.columns.id, params.id),
    set: { title, body },
  })

  return redirect(`/posts/${params.id}`)
})

Notice that data does not contain the _method field -- the middleware removes it before your handler runs.

Step 6: Build a Reusable Helper

If you have many RESTful forms, create a helper function:

ts
function restfulForm(method: string, action: string, content: string) {
  let hiddenField = method !== 'GET' && method !== 'POST'
    ? `<input type="hidden" name="_method" value="${method}" />`
    : ''

  let formMethod = method === 'GET' ? 'get' : 'post'

  return `
    <form method="${formMethod}" action="${action}">
      ${hiddenField}
      ${content}
    </form>
  `
}

Use it in your templates:

ts
return html`
  ${new SafeHtml(restfulForm('DELETE', `/posts/${post.id}`, `
    <button type="submit">Delete</button>
  `))}

  ${new SafeHtml(restfulForm('PUT', `/posts/${post.id}`, `
    <input type="text" name="title" value="${post.title}" />
    <button type="submit">Update</button>
  `))}
`

Step 7: Customize the Field Name

If you prefer a different field name (e.g., to match another framework's convention):

ts
methodOverride({
  fieldName: '_httpMethod',
})

Update your forms to match:

html
<input type="hidden" name="_httpMethod" value="DELETE" />

What You Learned

  • How to set up formData and methodOverride middleware in the correct order.
  • How to create delete and update forms using hidden _method fields.
  • How to write handlers that respond to PUT and DELETE methods.
  • How to build a reusable helper for RESTful forms.

Next Steps

Released under the MIT License.