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/express
We 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';
export interface Todo {
id: string;
title: string;
isCompleted: boolean;
}
export interface GetTodoRequestParams {
id: string;
}
export const getTodoEndpoint = 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';
export const getTodoHandler = createHandler(getTodoEndpoint, async ({ request, response }) => {
// Get todo from our async todo service and put it in our response body
response.body = await getTodoById(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';
export const router = new Router({
// We just have one handler for now
handlers: [getTodoHandler],
});
Finally, we need to connect our router to our web server.
import express = require('express');
import * as bodyParser from 'body-parser';
import { toMiddleware } from '@typepoint/express';
import { router } from './router';
async function run() {
const app = express();
const port = 3001;
app.use(bodyParser.json());
const handlerMiddleware = 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';
export const client = new Client({
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';
async function showTodo() {
const response = await client.fetch(getTodoEndpoint, {
params: {
id: '1',
},
});
const todo = 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/react
Before 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 * as React from 'react';
import * as ReactDOM from 'react-dom';
import { TypePointClient } from '@typepoint/client';
import { TypePointProvider } from '@typepoint/react';
import { App } from './app';
const client = new TypePointClient({
// Path to API server, normally this will be an environment variable
server: 'http://localhost:3001',
});
export const AppWithProviders = React.memo(() => (
<TypePointProvider client={client}>
<App />
</TypePointProvider>
));
const root = 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
.
import React from 'react';
import { useEndpoint } from '@typepoint/react';
import { getTodosEndpoint } from '../shared/endpoints/getTodosEndpoint';
const TodoApp = () => {
const { response } = useEndpoint(getTodosEndpoint, {});
const todos = response?.body ?? [];
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
};
export default TodoApp;
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.
const TodoApp = () => {
const { response } = useEndpoint(getTodosEndpoint, {});
const { response, loading, error } = useEndpoint(getTodosEndpoint, {});
if (loading) {
return <Spinner />;
}
if (error) {
return <div>Something went wrong!</div>;
}
const todos = response?.body ?? [];
return (
<ul>
{todos.map((todo) => (
<li key={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.
import React from 'react';
import { useEndpointLazily } from '@typepoint/react';
import { addTodoEndpoint } from '../shared/endpoints/addTodoEndpoint';
/**
* Component which can create a new todo.
*/
const NewTodoInput = () => {
const [title, setTitle] = useState('');
const { fetch: addTodo } = useEndpointLazily(addTodoEndpoint);
const addTodoWithTitle = useCallback((title: string) => {
addTodo({ body: { title } });
}, [addTodo]);
return (
<div>
<input type="text" value=>
<button type="button" onClick={addTodoWithTitle}>Add</button>
</div>
);
};
export default TodoApp;
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! 🌩