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
- A Remix project with a working server (see the Getting Started guide).
Step 1: Set Up the Middleware
The methodOverride middleware needs formData to come first, since it reads from the parsed form data:
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:
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:
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:
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
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:
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:
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):
methodOverride({
fieldName: '_httpMethod',
})Update your forms to match:
<input type="hidden" name="_httpMethod" value="DELETE" />What You Learned
- How to set up
formDataandmethodOverridemiddleware in the correct order. - How to create delete and update forms using hidden
_methodfields. - How to write handlers that respond to PUT and DELETE methods.
- How to build a reusable helper for RESTful forms.
Next Steps
- API Reference -- Full details on options and behavior.
- form-data-middleware -- Parsing form data that method override reads.
- csrf-middleware -- Add CSRF protection to your forms.