Defining API Endpoints
🔗 Defining API Endpoints
Endpoint definitions are one of the most powerful abstractions available, due to their expressivity, simplicity, and far-reaching effects. After hooking up our definition, we get to benefit from automated server-side request validation, auto-generated React hooks, and support for an API handler that we can customize.
In order to define an endpoint, we can expand the same core type file we created for our type, /core/src/types/todo.ts. You can of course create stand-alone APIs that aren’t related to a type; just follow the conventions here in a new file in the core types folder.
- The default export of a
/core/src/typesfile is treated as an object of endpoint definitions. You can see how the exports are used and merged in the core type index. - All top-level definition attributes (
kind,url,method, etc.) must be defined. Endpoint definitions use Typescript’sas conststatement, therefore all definitions must be alike in structure.as constlets us use our object at runtime, while establishing narrow types for our overall usage with Typescript related features. In other words, if we had only defined the followingexport defaultconstruct as a Typescript type, we wouldn’t get to use any of its details at runtime. Instead, creating an object that usesas constis more or less the best of both worlds for our needs.
import { ApiOptions, EndpointType } from './api';
import { Void } from '../util';
// Typescript type resides here, then we export the API definition...
export default {
postTodo: {
kind: EndpointType.MUTATION,
url: 'todos',
method: 'POST',
opts: {} as ApiOptions,
queryArg: { task: '' as string },
resultType: { id: '' as string }
},
putTodo: {
kind: EndpointType.MUTATION,
url: 'todos',
method: 'PUT',
opts: {} as ApiOptions,
queryArg: { done: true as boolean },
resultType: { success: true as boolean }
},
getTodos: {
kind: EndpointType.QUERY,
url: 'todos',
method: 'GET',
opts: {} as ApiOptions,
queryArg: {} as Void,
resultType: [] as ITodo[]
},
getTodoById: {
kind: EndpointType.QUERY,
url: 'todos/:id',
method: 'GET',
opts: {} as ApiOptions,
queryArg: { id: '' as string },
resultType: {} as ITodo
},
deleteTodo: {
kind: EndpointType.MUTATION,
url: 'todos/:id',
method: 'DELETE',
opts: {} as ApiOptions,
queryArg: { id: '' as string },
resultType: { success: true as boolean }
}
} as const;
kind: MUTATION or QUERY. In practice QUERY is used for all GET requests. MUTATION is used for everything else. When we use MUTATION, our usage of the database inside API handlers is automatically wrapped in a transaction.url: URLs are supported with common path and query param usage; these properties must be provided in the queryArgs object and passed on calling the endpoint. For example,todos/:idwould expect us to haveidlisted inqueryArgs. Alternatively we could writetodos?id=:id, depending on your needs. In the API handler, these properties are available withprops.event.pathParametersorprops.event.queryParameters, respectively.method: GET, PUT, POST, DELETEopts: Optional configurations. Listed here.queryArg: A combination of path, query, and request body parameters. This determines what we require to make a request from the front-end, and what is available to us in the API’s handler.resultType: The API handler must return something matching this type, or throw an error.- A negative side-effect of using
as constrequires us to define types for each property inqueryArgandresultType(i.e.id: '' as string). This is so our IDE can handle both generic and narrow typing of the endpoint definitions; this is important when we go to build and use hooks and API handlers. - Defining
{} as VoidforqueryArgorresultTypewill supply API handlers and React hooks with the correct types when no parameters are necessary.
Extend the Type exports
Once you’ve created your API definition, you need to hook it in to the merged API definitions. You can find the merged definitions in /core/src/types/index.ts.
- Import your new type file
- Add its reference to
siteApiRef - Export the reference from the index