Skip to content

Conversation

@Sheraff
Copy link
Owner

@Sheraff Sheraff commented Feb 10, 2024

Server-side, this implementation is nice.

Each route can define every method (get, put, ...) in the same file and simply export them by name (get in the example below).

  • the makeMethod pass-through function helps with type-safety, making sure we're compatible with fastify
  • the schema key will take advantage of fastify's integrated validation, making sure we only send / receive data in the format we're expecting
  • Currently, if the schema doesn't match what the handler does, we have no feedback before runtime (thought unit tests could be enough on this)
// route file
import { makeMethod } from "server/api/helpers"

export const get = makeMethod({
	handler(request) {
		request.log.info("hello world")
		return { hello: "world" }
	},
	schema: {
		response: {
			200: {
				type: "object",
				properties: {
					hello: { type: "string" },
				},
				required: ["hello"],
				additionalProperties: false,
			},
		},
	},
})

A "main router" file simply accumulates all the routes and exports both

  • a fastify plugin that can be simply called with app.register(plugin)
  • a router type, importable by the client, to ensure E2E type-safety of network IOs
// main router file
import { routerToRoutes, type ApiRouterFromRouter } from "server/api/helpers"
import * as simpleOpen from "server/api/simpleOpen"
import * as simpleProtected from "server/api/simpleProtected"

const router = {
	"/api/hello": simpleOpen,
	"/api/protected": simpleProtected,
}

const ApiPlugin = routerToRoutes(router)
export type ApiRouter = ApiRouterFromRouter<typeof router>
export default ApiPlugin

Client-side, it's a little more messy and not solved yet.

  • Inference works, but it doesn't sort routes correctly into "need params" and "doesn't need params"
  • I still haven't seen a DX i like to plug this kind of thing into tanstack/query
  • if we need HEAD or OPTIONS for some reason, there is no good way of doing it
  • I haven't touched the mutations yet

Questions

  • should we make this work for "special endpoints" (those that don't return JSON data)?
  • should we make this work outside of tanstack/query (like a regular fetch)?

@Sheraff
Copy link
Owner Author

Sheraff commented Feb 12, 2024

2nd implementation

  • no single proxy (easier on TS), more like trpc on Next app router
  • minimal amount of code
  • makes full use of fastify ajv schema validation
// route file
import { procedure, type BaseDefinition } from "./helpers"

export const definition = {
	url: "/api/hello",
	method: "get",
	schema: {
		response: {
			200: {
				type: "object",
				properties: {
					hello: { type: "string" },
				},
				required: ["hello"],
				additionalProperties: false,
			},
		},
	},
} as const satisfies BaseDefinition

export default /* @__PURE__ */ procedure(definition, {
	handler(request, reply) {
		void reply.code(200).send({ hello: "world" })
	},
})
// main router file
import { pluginFromRoutes } from "server/api/next/helpers"
import simpleOpen from "server/api/next/open"

const ApiPlugin = pluginFromRoutes([simpleOpen])

export default ApiPlugin
// client
import { definition } from "server/api/next/open"

const { data: next } = useApiQuery(definition, {
	Headers: { "x-id": "123" },
	Querystring: { id: "yoo" },
})

TODO:

  • This method requires having a shared bit of runtime code (definition). This means
    • either this code is placed separately, like in /shared, but to find the implementation of a route from the client we have to use "find references" instead of cmd+click (go to definition), and editing a route requires editing in multiple packages
    • or this code is placed inside /server but since server isn't built for "consumption", aliased imports aren't resolved properly... so we need to use relative imports like nowhere else in the project, or add a build step specifically for these? or add an alias in the vite config?
    • or the entire route implementation (definition + fastify handlers) needs to be placed in /shared, which might make access to other "server stuff" more complicated, like any access to a DB would have to go through some sort of context injected by /server
  • can we have "hot module reloading" that invalidates QueryClient when changing endpoint implementation on the server?

@Sheraff Sheraff changed the title WIP shared API types w/ inference, validation, E2E type safety Shared API types w/ inference, validation, E2E type safety Feb 13, 2024
@Sheraff Sheraff merged commit f87a867 into main Feb 13, 2024
@Sheraff Sheraff deleted the api-shared-types branch February 13, 2024 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants