Skip to content

Latest commit

 

History

History
156 lines (130 loc) · 4.37 KB

adding-custom-endpoints.md

File metadata and controls

156 lines (130 loc) · 4.37 KB

Adding custom endpoints

While Hatchify gives you a lot of power out of the box many applications, especially as they grow in complexity, need to apply custom rules and logic to their CRUD operations. Hatchify is prepared for this as well, allowing you to easily and flexibly override any of the default behavior to fit your needs. Even though you have customized the solution you can still use many of the Hatchify helper functions and features to accelerate development.

This is helpful if you need to:

Prerequisites

If you are using Koa, we suggest using the Koa Router to help create custom routes easily:

npm install @koa/router
npm install @types/koa__router --save-dev

Example

Given a pre-existing schema Case and some seeded data, going to http://localhost:3000/api/cases responds with something like:

{
  "jsonapi": {
    "version": "1.0"
  },
  "data": [
    {
      "type": "Case",
      "id": "705932d9-72ed-473b-bd8a-a60ff75d3122",
      "attributes": {
        "status": "deleted"
      }
    },
    {
      "type": "Case",
      "id": "20ffbc05-df92-46ed-b14e-a694c595c39d",
      "attributes": {
        "status": "open"
      }
    }
  ],
  "meta": {
    "unpaginatedCount": 2
  }
}

Now we want to add a custom route override to enforce validation and/or add other logic on the autogenerated route, while keeping the Hatchify helpers to parse, fetch and serialize before responding.

import querystring from "node:querystring"
import Koa from "koa"
import KoaRouter from "@koa/router" // 👀
import { hatchifyKoa, UnexpectedValueError } from "@hatchifyjs/koa"
import * as Schemas from "../schemas"

const app = new Koa()
const hatchedKoa = hatchifyKoa(Schemas, {
  prefix: "/api",
  database: {
    uri: process.env.DB_URI,
  },
})

// Creating a Koa Router
const router = new KoaRouter() // 👀

// Adding the custom route (or override the Hatchify one if already exists)
router.get("/api/cases", async function getCases(ctx): Promise<void> {
  const { query } = ctx
  const parseQuery = hatchedKoa.parse.Case.findAndCountAll
  const fetchCases = hatchedKoa.orm.models.Case.findAndCountAll
  const serializeResponse = hatchedKoa.serialize.Case.findAndCountAll

  // Here is a good opportunity to manipulate the query or add input validation:
  if (query["filter[status]"] === "deleted") {
    throw [
      new UnexpectedValueError({
        detail: "Querying for deleted cases is disallowed.",
        parameter: "filter[status]",
      }),
    ]
  }

  // Another example could be when the UI shows one column for `name` and sorting it sends `{ sort: "name" }`
  // which we might want to change to `{ sort: "lastName,firstName,middleInitial" }`

  // Here we rewrite the query string to force deleted cases out of the query:
  const findOptions = await parseQuery(querystring.stringify({ ...query, "filter[status][$ne]": "deleted" }))

  // `findOptions` is the query object we pass to `Sequelize`.
  // This is where you might want to enforce more complex AND/OR logic
  // or filter data the user is unauthorized to fetch.

  const cases = await fetchCases(findOptions)

  // Here you might want to adjust the returned data before it is serialized and returned to the client.

  ctx.body = await serializeResponse(cases, findOptions.attributes)
})
;(async () => {
  app.use(router.routes()) // 👀
  app.use(hatchedKoa.middleware.allModels.all)

  app.listen(3000, () => {
    console.log("Started on http://localhost:3000")
  })
})()

Now hitting http://localhost:3000/api/cases again filters out deleted cases:

{
  "jsonapi": {
    "version": "1.0"
  },
  "data": [
    {
      "type": "Case",
      "id": "20ffbc05-df92-46ed-b14e-a694c595c39d",
      "attributes": {
        "status": "open"
      }
    }
  ],
  "meta": {
    "unpaginatedCount": 1
  }
}

and querying for deleted cases specifically by going to http://localhost:3000/api/cases?filter[status]=deleted returns our custom error message:

{
  "jsonapi": {
    "version": "1.0"
  },
  "errors": [
    {
      "status": 422,
      "code": "unexpected-value",
      "title": "Unexpected value.",
      "detail": "Querying for deleted cases is disallowed.",
      "source": {
        "parameter": "filter[status]"
      }
    }
  ]
}