Form Routes
Theform() helper creates routes for displaying and handling HTML form submissions:
import { route, form } from 'remix/fetch-router/routes'
let routes = route({
home: '/',
contact: form('/contact'),
})
type Routes = typeof routes
// {
// home: Route<'ANY', '/'>
// contact: {
// index: Route<'GET', '/contact'> - Shows the form
// action: Route<'POST', '/contact'> - Handles form submission
// }
// }
index(GET) - Shows the formaction(POST) - Handles the form submission
The URL pattern for the form
Basic Form Example
import { createRouter } from 'remix/fetch-router'
import { route, form } from 'remix/fetch-router/routes'
import { formData } from 'remix/form-data-middleware'
import { html } from 'remix/html-template'
import { createHtmlResponse } from 'remix/response/html'
let routes = route({
home: '/',
contact: form('/contact'),
})
let router = createRouter({
middleware: [formData()],
})
router.map(routes, {
actions: {
home() {
return createHtmlResponse(html`
<html>
<body>
<h1>Home</h1>
<a href="${routes.contact.index.href()}">Contact Us</a>
</body>
</html>
`)
},
contact: {
actions: {
// GET /contact - shows the form
index() {
return createHtmlResponse(html`
<html>
<body>
<h1>Contact Us</h1>
<form method="POST" action="${routes.contact.action.href()}">
<label for="name">Name</label>
<input type="text" name="name" required />
<label for="email">Email</label>
<input type="email" name="email" required />
<label for="message">Message</label>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
</body>
</html>
`)
},
// POST /contact - handles the form submission
action({ get }) {
let formData = get(FormData)
let name = formData.get('name') as string
let email = formData.get('email') as string
let message = formData.get('message') as string
// Process the form data (send email, save to database, etc.)
console.log({ name, email, message })
return createHtmlResponse(html`
<html>
<body>
<h1>Thanks!</h1>
<p>We received your message, ${name}. We'll get back to you at ${email}.</p>
<a href="${routes.home.href()}">Back to Home</a>
</body>
</html>
`)
},
},
},
},
})
Form Data Middleware
Use theformData() middleware to parse form submissions:
import { formData } from 'remix/form-data-middleware'
let router = createRouter({
middleware: [formData()],
})
- Parses
application/x-www-form-urlencodedandmultipart/form-datarequest bodies - Stores the parsed FormData in request context
- Makes it available via
context.get(FormData)
Accessing Form Data
Access form fields using the FormData API:router.post(routes.contact.action, ({ get }) => {
let formData = get(FormData)
// Get a single value
let name = formData.get('name') as string
// Get all values for a field (e.g., checkboxes)
let interests = formData.getAll('interests') as string[]
// Check if a field exists
if (formData.has('subscribe')) {
console.log('User wants to subscribe')
}
// Iterate over all fields
for (let [key, value] of formData.entries()) {
console.log(`${key}: ${value}`)
}
return new Response('OK')
})
Custom Form Method
Change the form submission method:let routes = route({
profile: form('/profile', {
formMethod: 'PUT',
}),
})
type Routes = typeof routes
// {
// profile: {
// index: Route<'GET', '/profile'>
// action: Route<'PUT', '/profile'>
// }
// }
HTML forms only support GET and POST. To use PUT, PATCH, or DELETE, you’ll need to use JavaScript
fetch() or the methodOverride middleware with a hidden input field.Custom Route Names
Customize the route names:let routes = route({
login: form('/login', {
names: {
index: 'page',
action: 'submit',
},
}),
})
type Routes = typeof routes
// {
// login: {
// page: Route<'GET', '/login'>
// submit: Route<'POST', '/login'>
// }
// }
router.map(routes.login, {
actions: {
page() { /* GET /login */ },
submit({ get }) { /* POST /login */ },
},
})
File Uploads
Handle file uploads with theformData() middleware:
let routes = route({
upload: form('/upload'),
})
router.map(routes.upload, {
actions: {
index() {
return createHtmlResponse(html`
<html>
<body>
<h1>Upload File</h1>
<form method="POST" action="${routes.upload.action.href()}" enctype="multipart/form-data">
<label for="file">File</label>
<input type="file" name="file" required />
<label for="description">Description</label>
<input type="text" name="description" />
<button type="submit">Upload</button>
</form>
</body>
</html>
`)
},
async action({ get }) {
let formData = get(FormData)
let file = formData.get('file') as File
let description = formData.get('description') as string
console.log(`File: ${file.name}, Size: ${file.size}, Type: ${file.type}`)
console.log(`Description: ${description}`)
// Read file contents
let contents = await file.text()
// Or: let buffer = await file.arrayBuffer()
return createHtmlResponse(html`
<html>
<body>
<h1>Upload Complete</h1>
<p>Uploaded: ${file.name} (${file.size} bytes)</p>
<a href="${routes.upload.index.href()}">Upload Another</a>
</body>
</html>
`)
},
},
})
Remember to set
enctype="multipart/form-data" on forms that upload files.Resource Routes
Theresources() helper creates a full set of RESTful routes for a collection:
import { route, resources } from 'remix/fetch-router/routes'
let routes = route({
posts: resources('posts'),
})
type Routes = typeof routes.posts
// {
// index: Route<'GET', '/posts'> - List all posts
// new: Route<'GET', '/posts/new'> - Show form to create a post
// show: Route<'GET', '/posts/:id'> - Show a single post
// create: Route<'POST', '/posts'> - Create a new post
// edit: Route<'GET', '/posts/:id/edit'> - Show form to edit a post
// update: Route<'PUT', '/posts/:id'> - Update a post
// destroy: Route<'DELETE', '/posts/:id'> - Delete a post
// }
The base URL pattern for the resource
Configuration options for the resource routes
Show ResourcesOptions properties
Show ResourcesOptions properties
Only generate these routes. Example:
['index', 'show']Exclude these routes from generation. Example:
['destroy']The parameter name for individual resources. Default:
'id'Custom names for the generated routes. Example:
{ index: 'list', show: 'view' }ResourcesMethod values: 'index', 'new', 'show', 'create', 'edit', 'update', 'destroy'
Full Resources Example
import { createRouter } from 'remix/fetch-router'
import { route, resources } from 'remix/fetch-router/routes'
import { formData } from 'remix/form-data-middleware'
let routes = route({
posts: resources('posts'),
})
let router = createRouter({
middleware: [formData()],
})
router.map(routes.posts, {
actions: {
// GET /posts - List all posts
index() {
let posts = db.posts.findAll()
return Response.json(posts)
},
// GET /posts/new - Show form to create a post
new() {
return createHtmlResponse(html`
<form method="POST" action="${routes.posts.create.href()}">
<input type="text" name="title" />
<button type="submit">Create</button>
</form>
`)
},
// GET /posts/:id - Show a single post
show({ params }) {
let post = db.posts.findById(params.id)
if (!post) {
return new Response('Not Found', { status: 404 })
}
return Response.json(post)
},
// POST /posts - Create a new post
create({ get }) {
let formData = get(FormData)
let post = db.posts.create({
title: formData.get('title') as string,
})
return Response.json(post, { status: 201 })
},
// GET /posts/:id/edit - Show form to edit a post
edit({ params }) {
let post = db.posts.findById(params.id)
if (!post) {
return new Response('Not Found', { status: 404 })
}
return createHtmlResponse(html`
<form method="POST" action="${routes.posts.update.href({ id: params.id })}">
<input type="hidden" name="_method" value="PUT" />
<input type="text" name="title" value="${post.title}" />
<button type="submit">Update</button>
</form>
`)
},
// PUT /posts/:id - Update a post
update({ params, get }) {
let formData = get(FormData)
let post = db.posts.update(params.id, {
title: formData.get('title') as string,
})
return Response.json(post)
},
// DELETE /posts/:id - Delete a post
destroy({ params }) {
db.posts.delete(params.id)
return new Response(null, { status: 204 })
},
},
})
Limiting Routes
Generate only the routes you need:let routes = route({
posts: resources('posts', {
only: ['index', 'show', 'create'],
}),
})
type Routes = typeof routes.posts
// {
// index: Route<'GET', '/posts'>
// show: Route<'GET', '/posts/:id'>
// create: Route<'POST', '/posts'>
// }
let routes = route({
posts: resources('posts', {
exclude: ['destroy', 'edit', 'update'],
}),
})
type Routes = typeof routes.posts
// {
// index: Route<'GET', '/posts'>
// new: Route<'GET', '/posts/new'>
// show: Route<'GET', '/posts/:id'>
// create: Route<'POST', '/posts'>
// }
Custom Parameter Name
Change the parameter name from:id:
let routes = route({
posts: resources('posts', {
param: 'postId',
}),
})
type Routes = typeof routes.posts
// {
// ...
// show: Route<'GET', '/posts/:postId'>
// update: Route<'PUT', '/posts/:postId'>
// destroy: Route<'DELETE', '/posts/:postId'>
// }
router.get(routes.posts.show, ({ params }) => {
// params.postId instead of params.id
return Response.json({ id: params.postId })
})
Custom Route Names
let routes = route({
posts: resources('posts', {
only: ['index', 'show'],
names: {
index: 'list',
show: 'view',
},
}),
})
type Routes = typeof routes.posts
// {
// list: Route<'GET', '/posts'>
// view: Route<'GET', '/posts/:id'>
// }
Nested Resources
Create nested resource routes:let routes = route({
brands: {
...resources('brands', { only: ['index', 'show'] }),
products: resources('brands/:brandId/products', {
only: ['index', 'show', 'create'],
}),
},
})
type Routes = typeof routes.brands
// {
// index: Route<'GET', '/brands'>
// show: Route<'GET', '/brands/:id'>
// products: {
// index: Route<'GET', '/brands/:brandId/products'>
// show: Route<'GET', '/brands/:brandId/products/:id'>
// create: Route<'POST', '/brands/:brandId/products'>
// }
// }
router.map(routes.brands, {
actions: {
index() {
return Response.json(db.brands.findAll())
},
show({ params }) {
return Response.json(db.brands.findById(params.id))
},
products: {
actions: {
index({ params }) {
// params.brandId is available
return Response.json(db.products.findByBrand(params.brandId))
},
show({ params }) {
// Both params.brandId and params.id are available
return Response.json(db.products.findById(params.id))
},
create({ params, get }) {
let formData = get(FormData)
let product = db.products.create({
brandId: params.brandId,
name: formData.get('name') as string,
})
return Response.json(product, { status: 201 })
},
},
},
},
})
Singleton Resources
For resources that don’t belong to a collection (like a user profile), useresource():
import { resource } from 'remix/fetch-router/routes'
let routes = route({
profile: resource('profile'),
})
type Routes = typeof routes.profile
// {
// new: Route<'GET', '/profile/new'> - Show form to create profile
// show: Route<'GET', '/profile'> - Show the profile
// create: Route<'POST', '/profile'> - Create the profile
// edit: Route<'GET', '/profile/edit'> - Show form to edit profile
// update: Route<'PUT', '/profile'> - Update the profile
// destroy: Route<'DELETE', '/profile'> - Delete the profile
// }
Singleton resources don’t have an
index route and don’t use :id parameters.let routes = route({
profile: resource('profile', {
only: ['show', 'edit', 'update'],
}),
})
router.map(routes.profile, {
actions: {
show({ get }) {
let session = get(Session)
let userId = session.get('userId')
let profile = db.profiles.findByUserId(userId)
return Response.json(profile)
},
edit({ get }) {
let session = get(Session)
let userId = session.get('userId')
let profile = db.profiles.findByUserId(userId)
return createHtmlResponse(html`
<form method="POST" action="${routes.profile.update.href()}">
<input type="text" name="name" value="${profile.name}" />
<button type="submit">Update</button>
</form>
`)
},
update({ get }) {
let session = get(Session)
let userId = session.get('userId')
let formData = get(FormData)
let profile = db.profiles.update(userId, {
name: formData.get('name') as string,
})
return Response.json(profile)
},
},
})
Next Steps
Middleware
Add form validation and parsing with middleware
Controllers
Organize resource routes with controllers