Skip to content

Instantly share code, notes, and snippets.

@if1live
Created July 18, 2020 17:38
Show Gist options
  • Save if1live/b6d0a7c7e88cf7f4180e1b0b65e2b905 to your computer and use it in GitHub Desktop.
Save if1live/b6d0a7c7e88cf7f4180e1b0b65e2b905 to your computer and use it in GitHub Desktop.
API spec based api server/client library
import yup = require('yup');
import express from 'express';
import fetch, { Response } from 'node-fetch';
type Method = 'get' | 'post' | 'delete' | 'put';
interface Api<Req, Resp> {
name: string;
method: Method;
// url = resource + page
resource: string;
page: string;
schema: yup.Schema<Req>;
}
class MyRequest<T> {
constructor(public readonly body: T) { }
}
type ClientFunction<T> = T extends Api<infer Req, infer Resp>
? (body: Req) => Promise<Resp>
: never;
type ControllerFunction<T> = T extends Api<infer Req, infer Resp>
? (req: MyRequest<Req>) => Promise<Resp>
: never;
type Controller<T> = {
[P in keyof T]: ControllerFunction<T[P]>;
}
type Client<T> = {
[P in keyof T]: ClientFunction<T[P]>;
}
interface GetReq {
user_uid: string;
}
const userGetSchema = yup.object().shape<GetReq>({
user_uid: yup.string().required(),
}).required();
interface UpdateReq {
user_uid: string;
name: string;
}
const userUpdateSchema = yup.object().shape<UpdateReq>({
user_uid: yup.string().required(),
name: yup.string().required(),
}).required();
interface UserModel {
user_uid: string;
name: string;
}
const userGetSpec: Api<GetReq, UserModel> = {
name: 'get',
method: 'get',
resource: '/user',
page: '/get',
schema: userGetSchema,
};
const userUpdateSpec: Api<UpdateReq, UserModel> = {
name: 'update',
method: 'post',
resource: '/user',
page: '/update',
schema: userUpdateSchema,
};
interface UserApi {
get: typeof userGetSpec;
update: typeof userUpdateSpec;
}
const userApi: UserApi = {
get: userGetSpec,
update: userUpdateSpec,
};
const users: UserModel[] = [
{ user_uid: '1', name: 'first' },
{ user_uid: '2', name: 'second' },
];
const createUserController = (): Controller<UserApi> => ({
get: async (req) => {
const { user_uid } = req.body;
const user = users.find(x => x.user_uid === user_uid);
if (!user) { throw new Error('not found'); }
return user;
},
update: async (req) => {
const { user_uid, name } = req.body;
const user = users.find(x => x.user_uid === user_uid);
if (!user) { throw new Error('not found'); }
user.name = name;
return user;
},
});
const UserController: new () => Controller<UserApi> = function () {
return createUserController();
} as any;
class BaseClient {
constructor(protected readonly host: string) { }
protected handle<Req, Resp>(
spec: Api<Req, Resp>,
) {
const fn: ClientFunction<Api<Req, Resp>> = async (req) => {
const { method, resource, page } = spec;
const url = `${this.host}${resource}${page}`;
let resp: Response;
if (method === 'get') {
const params = new URLSearchParams();
const keys = Object.keys(req);
for (const key of keys) {
params.set(key, (req as any)[key]);
}
resp = await fetch(`${url}?${params.toString()}`);
} else {
resp = await fetch(url, {
method: method,
body: JSON.stringify(req),
headers: {
'Content-Type': 'application/json',
},
});
}
const text = await resp.text();
try {
return JSON.parse(text) as Resp;
} catch (e) {
throw e;
}
};
return fn;
}
}
class UserClient extends BaseClient implements Client<UserApi> {
public get = this.handle(userGetSpec);
public update = this.handle(userUpdateSpec);
}
function registerApi<Req, Resp>(
router: express.Router,
spec: Api<Req, Resp>,
handler: ControllerFunction<Api<Req, Resp>>,
) {
const { method, page, schema } = spec;
router[method](page, async (req, res) => {
const raw = {
...req.query,
...req.body,
};
try {
const body = await schema.validate(raw);
const resp = await handler(new MyRequest(body));
res.json(resp);
} catch (e) {
res.status(500).json(e);
}
});
}
const app = express();
app.use(express.json({}));
app.use(express.urlencoded({ extended: true }));
const userController = new UserController();
const userKeys = Object.keys(userApi) as Array<keyof UserApi>;
const userRouter = express.Router();
for (const key of userKeys) {
const api = userApi[key];
const fn = userController[key];
registerApi(userRouter, api as any, fn);
}
app.use('/user/', userRouter);
app.listen(3000, async () => {
console.log('listen 127.0.0.1:3000');
const client = new UserClient('http://127.0.0.1:3000');
console.log('update', await client.update({ user_uid: '1', name: 'hello' }));
console.log('get', await client.get({ user_uid: '1' }));
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment