Skip to content

Request Handling

API client requests are specified with a combination of HTTP verbs, endpoint paths and parameters, and query strings. For example, a request for a specific item would be a GET request for /resources/items/<someId> and a request to update a look would be a PATCH request to /resources/looks<someLookId>/ with look-specific values in the request body.

Verbs follow these rules:

  • GET - retrieve all resources for the given entity; if id is specified, retrieves a single resource
  • POST - create a new ‘stub’ resource and return an id
  • PATCH - update a specific resource (requires id parameter)
  • DELETE - delete a resource (requires id parameter)

Request endpoints are managed by the Remix file-based routing rules, nested under the #app/routes/resources+ subdirectory and organized by entity. For example, a looks request that does not include an id parameter would route to #app/routes/resources+/looks.tsx, and one with an id parameter routes to #app/routes/resources+/looks.$id.tsx.

By Remix convention, GET requests are handled by loaders, and POST, PUT, PATCH and DELETE requests by actions (we’re not currently using PUT requests in our application).

With a few exceptions, requests must always include two headers:

  • Authorization: Bearer {token}
    • The token value will be returned in the login response.
  • Accept: application/app.louvelle.v1+json
    • NOTE: The Accept header is not currently enforced but will be before public launch.

The first step in all request handlers is to pass the request and params to validateRequest function exported by #app/utils/api.server.ts:

const result = await validateRequest(request, params)

This function passes the original request and params through a pipe function with these stages:

  • parseRequest parses the request body (if included) and sets the url, pathname, headers and other request values
  • validateAuthorization checks to make sure the request includes an Authorization header, and if so:
    • decodes and validates the token against our secret key
    • retrieves the user ID and access roles (customer, admin etc.)
    • ensures the token hasn’t expired and matches an active session
  • checkRBACRules makes sure the user is authorized to access the requested resources
    • loads the RBAC rules from #app/utils/rbac.server.ts
    • finds a rule that matches the entity name and request method
    • checks to see if the user role is found in the rule definition

If the pipeline succeeds, it returns a ParsedRequest object. Failures return one of the errors defined in #app/utils/errors.server.ts

After a successful trip through the pipeline, we can check to see if the returned ParsedRequest includes a body and validate it against an entity-specific Zod schema.

For example, the handler that receives PATCH requests for looks at #app/resources+/looks.$id.tsx would validate the request body with UpdateLookRequestSchema.

If any errors are found in Zod validation, the returned ZodError object will include an array of all invalid properties.

In general, only the resource owner is allowed to modify or delete a resource they created.

The entity-specific handler will check the entity’s ownerId property for a match with the requesting userId. If they don’t match, we’ll allow the request only if the requesting user has admin or superadmin roles.

If we’ve made it this far and haven’t returned an error, we can now take the appropriate action specified by the request in the first place (creating, updating, deleting etc.)

Of course, there is still time for things to go wrong!

For instance, here’s the last stage of a request to update a look:

action handler - #app/routes/resources+/looks.$id.tsx
...
const updateLookResult = await updateLook(lookId, updates)
if (R.isOk(updateLookResult)) {
return json(updateLookResult._0, 200)
} else {
return jsonFromErrorResult(updateLookResult)
}
...

If all has gone well, the original API request will get back a 200 status code and some JSON in the expected shape of an update to a look.

If we encountered any errors along the way, the original request will get back an Error object with an appropriate status code, a message about the error, and in the case of a ZodError, an array of validation issues (see jsonFromErrorResult for details)