API Routes
Enhance’s API routes are backend JSON routes designed for seamless client-side progressive enhancement. API routes are defined under app/api
and follow the same file-based routing conventions as app/pages
. JSON response values from API routes are automatically passed to corresponding page routes.
API routes are designed to handle requests made for text/html
, or application/json
from a single HTTP request handler. API routes by default should always export get
and post
handlers for working with HTML forms. API routes can also optionally export handlers for put
, patch
, and destroy
for use with client-side JavaScript fetch.
Example
This is an example of a get
request handler for the index API route.
export async function get (req) {
return {
json: {
favorites: ['coffee crisp', 'smarties']
}
}
}
Request
API routes always receive a request
object:
key | type | description |
---|---|---|
body | object |
{} if request has no body |
headers | object |
Request headers |
method | string |
Request method: GET , POST , PATCH , PUT , or DELETE |
params | object |
URL params (e.g. ‘cat’ in /app/api/cats/$cat.mjs ) |
path | string |
Root-relative path of the request URL |
query | object |
Request querystring parameters |
session | object |
Read of the request cookie |
Response
API routes always return a response
object:
key | type | description |
---|---|---|
json |
object |
Plain JS object returned as a JSON response, or initial state for a text/html request |
location |
string |
Redirect path to send client |
statusCode |
number |
Set the HTTP status code (default: 200 , aliases: code , and status ) |
cacheControl |
string |
Set the cache-control header |
headers |
object |
Set response headers as key/value pairs |
session |
object |
Writes passed object to the session |
Middleware
API routes have a lightweight middleware concept. Export an array of async function handlers, and they will be run in the order you define.
export let get = [one, two]
async function one (req) {
console.log('hi from one')
req.first = true
}
async function two (req) {
console.log('hi from two')
const second = false
return { json: [req.first, second] }
}
Protip: Exit middleware early by return
ing a response.
Lifecycle tutorial: incrementing a counter
To demonstrate building out dynamic API routes and then progressively enhancing them we’ll build an incrementing counter. First rough in the HTML.
Part 1: basic HTML form handling
- Create a new Enhance app
npx "@enhance/cli@latest" new ./counter-app
. This generates a starter project withapp/pages/index.html
, and static assets inpublic/
. - Create a form for incrementing at
app/elements/form-counter.mjs
:
export default function counter ({ html, state }) {
return html`
<form action=/count method=post>
<button>+1</button>
</form>
<pre>${JSON.stringify(state, null, 2)}</pre>
`
}
Note the handy <pre>
debugger on line 6!
And add the new custom element to app/pages/index.html
:
<form-counter></form-counter>
- Create an API route to read the current count at
app/api/index.mjs
with the following contents:
export async function get (req) {
const count = req.session.count || 0
return {
json: { count }
}
}
- Create an API route to handle
POST /count
by creating a fileapp/api/count.mjs
with the following:
export async function post (req) {
let count = req.session.count || 0
count += 1
return {
session: { count },
location: '/'
}
}
The function above reads the count from the session or sets a default value, increments count
, and then writes the session, and redirects to /
. Always redirect after a form post to prevent double form submission, and proper back-button behavior.
- Preview the counter by running
npm start
; you’ll see thestate
update in the handy<pre>
tag debugger we created in step 1.
Part 2: progressive enhancement
The form functions even when client JS isn’t available. This is the moment where we can improve the web consumer experience, and augment the form to work without a posting the form, and incurring a server round trip.
- Add a JSON result to
app/api/count.mjs
export async function post (req) {
let count = req.session.count || 0
count += 1
return {
session: { count },
json: { count },
location: '/'
}
}
Now anytime POST /count
receives a request for JSON it will get { count }
.
Create a completely vanilla JS upgrade for the custom element:
export class Counter extends HTMLElement {
constructor () {
super()
this.form = this.querySelector('form')
this.pre = this.querySelector('pre')
this.form.addEventListener('submit', this.addOne.bind(this))
}
async addOne (e) {
e.preventDefault()
const res = await fetch('/count', {
headers: { 'accept': 'application/json' },
method: 'post'
})
const json = await res.json()
this.pre.innerHTML = JSON.stringify(json, null, 2)
}
}
customElements.define('form-counter', Counter)
Any framework or library could be used but this example is to show those are optional; the nice thing about working with the low level code is there are no dependencies and this will work in all browsers forevermore.
Add the client script to the custom element, and reload to see the enhanced version.
export default function counter ({ html, state }) {
return html`
<form action=/count method=post>
<button>+1</button>
</form>
<pre>${JSON.stringify(state, null, 2)}</pre>
<script type=module src=/_public/form-counter.mjs></script>
`
}