If you're developing a client & server app both in TypeScript, you can leverage TypePoint to DRYly define your RESTful API endpoints to have strongly typed requests and responses, without duplicating or generating code.
- Strongly typed requests and responses, both on client and server!
- Endpoints are defined only once
- No code generation!
- Validation of request params and body
- Promise based request handlers & middleware
- Support for React Hooks
First we need to install the following packages:
npm add @typepoint/client @typepoint/server @typepoint/shared @typepoint/expressWe start by defining an endpoint in a shared folder. Our example will define an endpoint to return a Todo with a given id passed as a path param.
import{defineEndpoint,Empty}from'@typepoint/shared';exportinterfaceTodo{id: string;title: string;isCompleted: boolean;}exportinterfaceGetTodoRequestParams{id: string;}exportconstgetTodoEndpoint=defineEndpoint<GetTodoRequestParams,Empty,Todo[]>((path)=>path.literal('/api/todos/').param('id'),);Our getTodoEndpoint variable defines our endpoint, including:
- The HTTP method (defaults to
'GET') - The path to the endpoint:
'/api/todos/{id}' - The request params type:
GetTodoRequestParams - The request body type:
Empty(no body, because its a GET method) - The response body:
Todo
Next, let's create a handler for our endpoint in our server.
import{createHandler}from'@typepoint/server';import{getTodoEndpoint}from'../../shared/endpoints/getTodoHandler';import{getTodoById}from'./todoService';exportconstgetTodoHandler=createHandler(getTodoEndpoint,async({ request, response })=>{// Get todo from our async todo service and put it in our response bodyresponse.body=awaitgetTodoById(request.params.id);});Next we need to create a router in order to route requests to our handlers.
import{Router}from'@typepoint/server';import{getTodoHandler}from'./todos/getTodoHandler';exportconstrouter=newRouter({// We just have one handler for nowhandlers: [getTodoHandler],});Finally, we need to connect our router to our web server.
importexpress= require('express');import*asbodyParserfrom'body-parser';import{toMiddleware}from'@typepoint/express';import{router}from'./router';asyncfunctionrun(){constapp=express();constport=3001;app.use(bodyParser.json());consthandlerMiddleware=toMiddleware(router);app.use(handlerMiddleware);app.listen(port,()=>{console.log(`API Server running at http://localhost:${port}`);});}run().catch(console.error);Now if we run ts-node ./server we'll have a working server that will handle getting a todo.
On the front-end, we need to create a Client in order to make requests.
import{TypePointClient}from'@typepoint/client';exportconstclient=newClient({server: 'http://localhost:3001',});Now we have our TypePointClient created, we can use it anywhere in our front-end.
import{client}from'./typepoint';import{getTodoEndpoint}from'../shared/endpoints/getTodoEndpoint';asyncfunctionshowTodo(){constresponse=awaitclient.fetch(getTodoEndpoint,{params: {id: '1',},});consttodo=response.body;alert(todo.title);}The above client side code is completely typed. Trying to fetch from the getTodoEndpoint without passing the a string id as a param will cause a design/compile-time error. The response is also completely typed.
The @typepoint/react library provides react hooks to call your endpoints.
npm add @typepoint/reactBefore you can start using TypePoint React hooks, you'll need to wrap your root component with a TypePointProvider. This provides the client that the hooks will use.
import*asReactfrom'react';import*asReactDOMfrom'react-dom';import{TypePointClient}from'@typepoint/client';import{TypePointProvider}from'@typepoint/react';import{App}from'./app';constclient=newTypePointClient({// Path to API server, normally this will be an environment variableserver: 'http://localhost:3001',});exportconstAppWithProviders=React.memo(()=>(<TypePointProviderclient={client}><App/></TypePointProvider>));constroot=window.document.getElementById('root');ReactDOM.render(<AppWithProviders/>,root);useEndpoint is a hook that immediately fetches a given endpoint with the given params and or body.
importReactfrom'react';import{useEndpoint}from'@typepoint/react';import{getTodosEndpoint}from'../shared/endpoints/getTodosEndpoint';constTodoApp=()=>{const{ response }=useEndpoint(getTodosEndpoint,{});consttodos=response?.body??[];return(<ul>{todos.map((todo)=>(<likey={todo.id}>{todo.title}</li>))}</ul>);};exportdefaultTodoApp;The endpoint will not be fetched unnecessarily, only when the params change. In the above example no params are required so it will only be fetched once.
useEndpoint also returns a refetch function which can be called imperatively to refetch the endpoint with the last used params and body. This is useful for refreshing data.
const{ response, refetch }=useEndpoint(getTodosEndpoint,{});useEndpoint also returns the loading state of the request, as well as an error if there is one.
constTodoApp=()=>{const{ response }=useEndpoint(getTodosEndpoint,{});const{ response, loading, error }=useEndpoint(getTodosEndpoint,{});if(loading){return<Spinner/>;}if(error){return<div>Something went wrong!</div>;}consttodos=response?.body??[];return(<ul>{todos.map((todo)=>(<likey={todo.id}>{todo.title}</li>))}</ul>);};useEndpointLazily is just like useEndpoint except that it does not immediately fetch the endpoint, instead it provides a fetch function to let you fetch the given endpoint on demand.
importReactfrom'react';import{useEndpointLazily}from'@typepoint/react';import{addTodoEndpoint}from'../shared/endpoints/addTodoEndpoint';/** * Component which can create a new todo. */constNewTodoInput=()=>{const[title,setTitle]=useState('');const{fetch: addTodo}=useEndpointLazily(addTodoEndpoint);constaddTodoWithTitle=useCallback((title: string)=>{addTodo({body: { title }});},[addTodo]);return(<div><inputtype="text"value=><buttontype="button"onClick={addTodoWithTitle}>Add</button></div>
);
};exportdefaultTodoApp;Check out the examples repository for example apps that use TypePoint.
Got a problem or suggestion? Submit an issue!
Want to contribute? Fork the repository and submit a pull request! 🌩