#+TITLE: @apparts/api
#+DATE: [2019-02-07 Thu]
#+AUTHOR: Philipp Uhl
To tell the =@apparts/api= package, how to work with your API, you
have to overload the =Request= class. If you are using tokens for users
to be authenticated (e.g. when using =@apparts/login-server=) you also
need to overload the =Token= class and tell the =Request= class to use
the =Token= class in a custom authentication method (compare
=authUser= below).
Finally, give your new =Request= class to =useApi= and export the
functions that you recieve.
#+BEGIN_SRC js
import { useApi, Token, Request, ApiFnParams } from "@apparts/api";
const APIVERSION = 1;
const URL = DEV // eslint-disable-line no-undef
? "https://devendpoint/v/"
: "https://prodendpoint/v/";
const logout = () => {
// Log out user on 401 HTTP-Error
window.location.href = "/login?force=true";
};
class MyToken extends Token {
constructor(user) {
super(user);
// Tell Token where to find the users api token, should you
// already have one
this._apiToken = user.apiToken;
}
async renewAPIToken(user) {
// Tell Token how to renew the API Token
const apiToken = await get("user/apiToken").authPW(
user.email,
user.loginToken
);
return apiToken;
}
static getUserKey(user) {
// Tell Token how to identify users
return user.email;
}
}
class MyRequest extends Request {
getURL(apiVersion) {
// Tell Request what the URL prefix is
return URL + apiVersion;
}
getAPIVersion() {
// Tell Request what the default APIVersion is
return APIVERSION;
}
online() {
// Will be called, when a network-call succeded
}
authUser(user) {
// Define a method for authenticating with a user token.
// This will be called by you, when you want to authenticate with a user
this._auth = MyToken.getAPIToken(user);
return this;
}
async middleware() {
// Tell Request what to do on recieving not-yet caught errors, that should be
// handled globally.
this.on(410, () => alert("Your website is out of date, please reload it."));
this.on({ status: 401, error: "Token invalid" }, () => {
throw "Invalid token";
});
this.on(401, logout);
this.on(0, () => alert("We could not reach the server. Are you online?"));
}
}
const api = useApi(MyRequest);
const get = (...ps: ApiFnParams) => api.get(...ps) as unknown as MyRequest,
put = (...ps: ApiFnParams) => api.put(...ps) as unknown as MyRequest,
patch = (...ps: ApiFnParams) => api.patch(...ps) as unknown as MyRequest,
post = (...ps: ApiFnParams) => api.post(...ps) as unknown as MyRequest,
del = (...ps: ApiFnParams) => api.del(...ps) as unknown as MyRequest;
export { get, put, patch, post, del };
#+END_SRC
The functions =get=, =put=, =patch=, =post=, =del= have this
signature (exemplary for =get=):
- =get(url: , urlParams: <+array>): Request= :: The url can be
parameterized with =$1=, =$2=, ... Each =$=-prefixed number will be
replaced with the corresponding element from the array: =$1= will be
replaced with =urlParams[0]=, =$2= with =urlParams[1]=, and so on. A
parameter can be used multiple times in the =url=-string. If not all
or too many parameters are used, an error will be thrown.
The functions =get=, =put=, =patch=, =post=, =del= return a
=Request=. Requests have the following methods:
- =query(params: ): Request= :: Set query params as key value
pairs.
: r.query({ filter: "4", a: "b" }) // will be https://yourendpoint/path/to/endpoint?filter=4&a=b
- =data(data: ): Request= :: Set the body data
- =settings(settings: ): Request= :: Set =axios= settings
- =v(v: int): Request= :: Overwrite APIVersion, used for this request
- =auth(auth: ): Request= :: Set the =axios=-auth param
- =authPW(username: , password: ): Request= :: Use basic auth
- =authAPIKey(token: ): Request= :: Use bearer auth
- =on(status: <int | object>, next: ): Request= :: Attach an error handler:
- Status parameter:
- If used with =status= as an integer, matches all HTTP-Errors with
that status.
- If used with =status= as an object of the form ={ status: ,
error: }=, matches all HTTP-Errors with that status and
an object with a matching error-key in the body.
- When such an error occurs, =next(errorJson, error)=
will be called with =errorJson= being the parsed error and =error=
the raw =axios= error.
- Multiple error catchers can be appended. The first one to match
(in order of attaching) will be executed.
- When error has been caught, =catch= will be called, but receives
=false= as an error.
- =then(): Promise= :: Then
- =catch(): Promise= :: Catch
- =finally(): Promise= :: Finally
Example:
#+BEGIN_SRC js
try {
const resp = await put("users/$1/name")
.data({ name: "John" })
.userAuth(user)
.on({ status: 400, error: "Too short" }, () => {
alert("Please choose a longer username.");
})
.on({ status: 400, error: "Is taken" }, () => {
alert("This username is taken, already. Please choose a different username.");
});
} catch (e) {
// If e is not false, then, no error-catcher caught the error and
// you might want to take care of it
e && alert(e);
// Do, what you have to do on an error. Catch will be called, even
// when the error was caught by an error catcher. If you have some
// error-unspecific cleanup to do, this would be a good place:
/* setLoading(false); */
}
#+END_SRC
=@apparts/api= supports generating a fully typed TypeScript SDK to
access an API that is defined through an API description as generated
by =@apparts/prep=.
You might want to install =prettier= (=npm i -D prettier=) to
improve the output.
To generate the SDK, run the following:
#+BEGIN_SRC js
import * as prettier from "prettier";
const prettify = (src) => prettier.format(src, { parser: "typescript" });
// The API definition as output by the getApi function from @apparts/prep
import { testApi } from "./api-description.json";
import { genFile, EndpointDefinition } from "@apparts/api";
// Pipe to file or write to fs from here
console.log(prettify(genFile(testApi.routes as EndpointDefinition[], {
// includePaths?: string[][];
// Can be used to only include a subset of the routes in the output
// excludePaths?: string[][];
// Can be used to exclude a subset of the routes from the output
// emitNoSchema?: boolean;
// If true, schemas will not be generated, but typescript types still will be. (default: false)
})));
#+END_SRC
The resulting API SDK code exports the function =createApi= which
expects one parameter: the api as exported from =useApi(MyRequest)= as
setup above.
In your application:
#+BEGIN_SRC js
import { createApi } from "./"
// setup MyRequest, etc.
const apiRaw = useApi(MyRequest);
export const api = createApi(apiRaw);
#+END_SRC
The resulting api object contains all API endpoints in the following
manner:
An endpoint =GET /v/1/user/:userId/info= with the returns
- code 200, ==
- code 404, ={ error: "User not found" }=
- code 404, ={ error: "User info not found" }=
Can be accessed like this:
#+BEGIN_SRC js
try {
const res = await api.user.info.get({ params: { userId }, /* data, query /})
// Optional error catchers. Matching checked in order of function
// usage. So in this example, first the catcher for { status: 404,
// error: "user not found" } is checked, then { status: 404, error:
// "user info not found" }, at last all status 404 responses.
.on404UserNotFound(/ catcher fn /)
.on404UserInfoNotFound(/ catcher fn /)
.on404(/ catcher fn */) // catch all for code 404
// a Request object is returned, just with the normal
// get/put/post/etc functions from this package. Hence, you can call
// all helper functinos as wanted. E.g.:
.auth(user);
} catch (e) {
// If e is not false, then, no error-catcher caught the error and
// you might want to take care of it
e && alert(e);
}
#+END_SRC