Created
July 18, 2020 17:38
-
-
Save if1live/b6d0a7c7e88cf7f4180e1b0b65e2b905 to your computer and use it in GitHub Desktop.
API spec based api server/client library
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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