Simple client/server libraries to assist with creating a type declaration for an express API and using that type to automatically infer the return type of client fetch
requests.
Take the following sample express API and its corresponding client request:
/* server.ts */
import express from 'express';
const app = express();
app.get('/api/weather/:cityName', (req) => {
if (req.params.cityName.length < 3) {
return {
payload: { errorMessage: 'City name must have at least 3 characters' },
status: 400,
};
}
return {
payload: {
temperature: Math.floor(Math.random() * 5000) / 100,
/* other weather properties */
},
};
});
app.listen(process.env.PORT || 3000);
/* client.ts */
export const fetchWeather = async (cityName: string) => {
const response = await fetch(`/api/weather/${cityName}`, { method: 'get' });
const payload = await response.json();
if ('errorMessage' in payload) {
console.error(payload);
// Deal with validation errors
} else {
console.log(payload);
// Deal with weather data
}
};
These are the required steps to setup @express-typed-api
and automatically infer the payload
type:
-
API's type declaration. Declare a type for the API in a folder/project accessible to both client and server code, where
@express-typed-api/common
must be installed as a dependency:npm install --save @express-typed-api/common
import { EndpointHandler } from '@express-typed-api/common'; export type GetWeatherEndpoint = EndpointHandler< | { temperature: number; /* other weather properties */ } | { errorMessage: string; } >; export type WeatherApi = { '/api/weather/:cityName': { get: GetWeatherEndpoint; }; };
-
Server. Install
@express-typed-api/server
on the server project/folder and import the API's type declaration from the shared project/folder. Locate allapp.<http_method>
calls and extract the handlers into named functions. Create an API representation object containing the handlers as properties, indexed by the endpoints' path and method, making sure to type it with the API's type. Replace theapp.<http_method>
calls with a single call to thepublishApi
method, passing the API representation object as the second parameter.npm install --save @express-typed-api/server
/* server.ts */ import { publishApi } from '@express-typed-api/server'; import express from 'express'; import { GetWeatherEndpoint, WeatherApi } from '...'; // Import from shared project/folder const app = express(); const getWeatherEndpoint: GetWeatherEndpoint = (req) => { if (req.params.cityName.length < 3) { return { payload: { errorMessage: 'City name must have at least 3 characters' }, status: 400, }; } return { payload: { temperature: Math.floor(Math.random() * 5000) / 100, /* other weather properties */ }, }; }; const weatherApi: WeatherApi = { '/api/weather/:cityName': { get: getWeatherEndpoint, }, }; publishApi(app, weatherApi); app.listen(process.env.PORT || 3000);
-
Client. Install
@express-typed-api/client
on the client project/folder and import the API's type declaration from the shared project/folder. Get thetypedFetch
function by callinggetTypedFetch
with the API's type as its type parameter. Replace all thefetch
calls with thetypedFetch
function:npm install --save @express-typed-api/client
/* client.ts */ import { getTypedFetch } from '@express-typed-api/client'; import { WeatherApi } from '...'; // Import from shared project/folder const typedFetch = getTypedFetch<WeatherApi>(); export const fetchWeather = async (cityName: string) => { const response = await typedFetch( '/api/weather/:cityName', { method: 'get' }, { params: { cityName }, } ); const payload = await response.json(); if ('errorMessage' in payload) { console.error(payload); // Deal with validation errors } else { console.log(payload); // Deal with weather data } };
For more examples see the sample repository.
A list of the successfully published endpoints, each containing its path, method and handlers.
-
app: The
express()
orexpress.Router()
instance where the api endpoints will be published at. -
apiEndpoints: The API representation object, having the endpoints' handler as properties and the endpoints' path and method as keys.
-
options: Optional configuration parameters.
name type default description pathsPrefix string? undefined
A string that will be prepended to all endpoints' path
const app = express();
publishApi(app, {
'/my/path': {
get: (req, res, next) => {
/* ... */
},
post: (req, res, next) => {
/* ... */
},
// ...
},
});
app.listen(3000);
An instance of typedFetch, configured for the TApi
type.
-
TApi: Web API's type declaration, having the endpoints' handler as properties and the endpoints' path and method as keys.
-
options: Optional configuration parameters.
name type default description baseUrl string? undefined
A string that will be prepended to all fetch requests' URL
const typedFetch = getTypedFetch<MyApiType>();
The Promise that results from calling fetch
with the corresponding path and init parameters.
-
path: The target API endpoint's path.
-
init: An object containing custom settings that will be applied to the
fetch
request (i.e.headers
). Note that themethod
property is mandatory, as it is necessary to resolve the target endpoint's return type. -
payload: An optional object containing data that will be sent with the
fetch
request.name type default description jsonBody any? undefined
Only available when using typed request payload with jsonBody
. Non serialized request's body that will be stringified (i.e.JSON.stringify
) and sent to the server. Requires including theContent-Type
header withapplication/json
valueparams { [key: string]: string }? undefined
Key-value dictionary that can be used to replace express URL parameters in the request's URL (see paramsResponse
example). Mandatory when using typed request payload withquery
query { [key: string]: string }? undefined
Key-value dictionary that can be used to prepend query string parameters to the URL (see queryResponse
example). Mandatory when using typed request payload withparams
const response = await typedFetch('/my/path', { method: 'get' });
const paramsResponse = await typedFetch(
'/my/path/:name',
{ method: 'get' },
{ params: { name: 'Jena' } }
);
// Will generate a request URL of "/my/path/Jena"
const queryResponse = await typedFetch('/my/path', { method: 'get' }, { query: { name: 'Jena' } });
// Will generate a request URL of "/my/path?name=Jena"
@express-typed-api
allows specifying the type of the requests' payload (i.e. query
, params
and body
) by providing an optional second type parameter to EndpointHandler
, containing any combination of jsonBody
, params
and/or query
types.
import { EndpointHandler } from '@express-typed-api/common';
export type GetWeatherEndpoint = EndpointHandler<
| {
temperature: number;
/* other weather properties */
}
| {
errorMessage: string;
},
{
params: { cityName: string };
}
>;
export type WeatherApi = {
'/api/weather/:cityName': {
get: GetWeatherEndpoint;
};
};
The types will then be enforced on both client requests and server endpoint handlers: