Add form data middleware
Enable automatic form parsing by adding the form data middleware:This middleware automatically parses
app/router.ts
import { createRouter } from 'remix/fetch-router'
import { formData } from 'remix/form-data-middleware'
let middleware = [
formData(),
]
export let router = createRouter({ middleware })
application/x-www-form-urlencoded and multipart/form-data requests, making form data available via get(FormData).Create a basic form
Build an HTML form that posts to your server:
app/pages/contact.tsx
import { css } from 'remix/component'
import { Document } from '../layout.tsx'
interface ContactFormProps {
error?: string
success?: boolean
}
export function ContactForm({ error, success }: ContactFormProps) {
return (
<Document title="Contact Us">
<h1>Contact Us</h1>
{error && (
<div
mix={[
css({
padding: '1rem',
background: '#fee',
border: '1px solid #fcc',
borderRadius: '4px',
marginBottom: '1rem',
}),
]}
>
{error}
</div>
)}
{success && (
<div
mix={[
css({
padding: '1rem',
background: '#efe',
border: '1px solid #cfc',
borderRadius: '4px',
marginBottom: '1rem',
}),
]}
>
Thank you! Your message has been sent.
</div>
)}
<form method="POST" action="/contact">
<div mix={[css({ marginBottom: '1rem' })]}>
<label
for="name"
mix={[css({ display: 'block', marginBottom: '0.5rem' })]}
>
Name
</label>
<input
type="text"
id="name"
name="name"
required
mix={[
css({
width: '100%',
padding: '0.5rem',
border: '1px solid #ddd',
borderRadius: '4px',
}),
]}
/>
</div>
<div mix={[css({ marginBottom: '1rem' })]}>
<label
for="email"
mix={[css({ display: 'block', marginBottom: '0.5rem' })]}
>
Email
</label>
<input
type="email"
id="email"
name="email"
required
mix={[
css({
width: '100%',
padding: '0.5rem',
border: '1px solid #ddd',
borderRadius: '4px',
}),
]}
/>
</div>
<div mix={[css({ marginBottom: '1rem' })]}>
<label
for="message"
mix={[css({ display: 'block', marginBottom: '0.5rem' })]}
>
Message
</label>
<textarea
id="message"
name="message"
required
rows={5}
mix={[
css({
width: '100%',
padding: '0.5rem',
border: '1px solid #ddd',
borderRadius: '4px',
}),
]}
/>
</div>
<button
type="submit"
mix={[
css({
padding: '0.75rem 1.5rem',
background: '#0070f3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '1rem',
}),
]}
>
Send Message
</button>
</form>
</Document>
)
}
Handle form submissions
Create route handlers for both GET (show form) and POST (process form):
app/router.ts
import { createRouter } from 'remix/fetch-router'
import { formData } from 'remix/form-data-middleware'
import { routes } from 'remix/fetch-router/routes'
import { redirect } from 'remix/response/redirect'
import { render } from './utils/render.ts'
import { ContactForm } from './pages/contact.tsx'
export let appRoutes = routes({
contact: {
index: 'GET /contact',
submit: 'POST /contact',
},
})
export let router = createRouter({
middleware: [formData()],
})
// Show the form
router.get(appRoutes.contact.index, () => {
return render(<ContactForm />)
})
// Process form submission
router.post(appRoutes.contact.submit, ({ get }) => {
let form = get(FormData)
let name = form.get('name')?.toString() ?? ''
let email = form.get('email')?.toString() ?? ''
let message = form.get('message')?.toString() ?? ''
// Validate
if (!name || !email || !message) {
return render(
<ContactForm error="All fields are required" />,
{ status: 400 }
)
}
if (!email.includes('@')) {
return render(
<ContactForm error="Please enter a valid email address" />,
{ status: 400 }
)
}
// Process the form (send email, save to database, etc.)
console.log('Contact form submitted:', { name, email, message })
// Show success message
return render(<ContactForm success />)
})
Add field-level validation
Validate individual fields and show specific errors:Update your form component to show field-specific errors:Use validation in your handler:
app/utils/validation.ts
interface ValidationResult {
valid: boolean
errors: Record<string, string>
}
export function validateContactForm(formData: FormData): ValidationResult {
let errors: Record<string, string> = {}
let name = formData.get('name')?.toString() ?? ''
let email = formData.get('email')?.toString() ?? ''
let message = formData.get('message')?.toString() ?? ''
if (!name || name.trim().length < 2) {
errors.name = 'Name must be at least 2 characters'
}
if (!email || !email.includes('@')) {
errors.email = 'Please enter a valid email address'
}
if (!message || message.trim().length < 10) {
errors.message = 'Message must be at least 10 characters'
}
return {
valid: Object.keys(errors).length === 0,
errors,
}
}
app/pages/contact.tsx
interface ContactFormProps {
errors?: Record<string, string>
values?: Record<string, string>
success?: boolean
}
export function ContactForm({ errors = {}, values = {}, success }: ContactFormProps) {
return (
<Document title="Contact Us">
<h1>Contact Us</h1>
<form method="POST" action="/contact">
<div mix={[css({ marginBottom: '1rem' })]}>
<label for="name">Name</label>
<input
type="text"
id="name"
name="name"
value={values.name}
required
/>
{errors.name && (
<p mix={[css({ color: 'red', fontSize: '0.875rem', marginTop: '0.25rem' })]}>
{errors.name}
</p>
)}
</div>
{/* Similar for email and message */}
<button type="submit">Send Message</button>
</form>
</Document>
)
}
import { validateContactForm } from './utils/validation.ts'
router.post(appRoutes.contact.submit, ({ get }) => {
let form = get(FormData)
let validation = validateContactForm(form)
if (!validation.valid) {
return render(
<ContactForm
errors={validation.errors}
values={{
name: form.get('name')?.toString() ?? '',
email: form.get('email')?.toString() ?? '',
message: form.get('message')?.toString() ?? '',
}}
/>,
{ status: 400 }
)
}
// Process form...
return render(<ContactForm success />)
})
Use sessions for flash messages
Store temporary messages in sessions for the redirect-after-post pattern:
app/router.ts
import { session } from 'remix/session-middleware'
import { createCookieSessionStorage } from 'remix/session/cookie-storage'
import { redirect } from 'remix/response/redirect'
let sessionStorage = createCookieSessionStorage({
cookie: {
name: 'session',
secrets: ['your-secret-key'],
maxAge: 60 * 60 * 24 * 7, // 7 days
},
})
export let router = createRouter({
middleware: [
formData(),
session('session', sessionStorage),
],
})
// Show form with flash messages
router.get(appRoutes.contact.index, ({ get }) => {
let session = get(Session)
let success = session.get('success')
let error = session.get('error')
return render(<ContactForm success={success} error={error} />)
})
// Process and redirect
router.post(appRoutes.contact.submit, ({ get }) => {
let form = get(FormData)
let session = get(Session)
// Validate and process...
session.flash('success', true)
return redirect('/contact')
})
Handle checkboxes and radio buttons
Work with different input types:Process in your handler:
<form method="POST" action="/settings">
{/* Checkbox */}
<label>
<input
type="checkbox"
name="newsletter"
value="yes"
/>
Subscribe to newsletter
</label>
{/* Multiple checkboxes */}
<fieldset>
<legend>Interests</legend>
<label>
<input type="checkbox" name="interests" value="tech" />
Technology
</label>
<label>
<input type="checkbox" name="interests" value="design" />
Design
</label>
</fieldset>
{/* Radio buttons */}
<fieldset>
<legend>Account Type</legend>
<label>
<input type="radio" name="accountType" value="personal" />
Personal
</label>
<label>
<input type="radio" name="accountType" value="business" />
Business
</label>
</fieldset>
<button type="submit">Save Settings</button>
</form>
router.post('/settings', ({ get }) => {
let form = get(FormData)
// Checkbox (single)
let newsletter = form.get('newsletter') === 'yes'
// Multiple checkboxes
let interests = form.getAll('interests') // ['tech', 'design']
// Radio button
let accountType = form.get('accountType')?.toString() ?? 'personal'
console.log({ newsletter, interests, accountType })
return redirect('/settings?saved=true')
})
Add CSRF protection
Protect against cross-site request forgery:Add token to forms:
app/middleware/csrf.ts
import type { Middleware } from 'remix/fetch-router'
function generateToken(): string {
return Math.random().toString(36).substring(2)
}
export function csrf(): Middleware {
return async ({ get, request }, next) => {
let session = get(Session)
// Generate token for GET requests
if (request.method === 'GET') {
if (!session.has('csrfToken')) {
session.set('csrfToken', generateToken())
}
return next()
}
// Verify token for POST/PUT/PATCH/DELETE
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method)) {
let form = get(FormData)
let token = form.get('_csrf')?.toString()
let sessionToken = session.get('csrfToken')
if (!token || token !== sessionToken) {
return new Response('Invalid CSRF token', { status: 403 })
}
}
return next()
}
}
router.get('/contact', ({ get }) => {
let session = get(Session)
let csrfToken = session.get('csrfToken')
return render(<ContactForm csrfToken={csrfToken} />)
})
// In your form component
<form method="POST" action="/contact">
<input type="hidden" name="_csrf" value={csrfToken} />
{/* Other fields... */}
</form>
Form Best Practices
Always validate on the server
Client-side validation is good for UX, but server-side validation is essential for security.Preserve form values on errors
When validation fails, repopulate the form with submitted values so users don’t have to retype everything.Use proper HTTP methods
GETfor displaying formsPOSTfor creating resourcesPATCH/PUTfor updating resourcesDELETEfor removing resources