-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
use io-ts to validate params, query and body #4
Conversation
This is really nice! A common pattern is for routes to validate the Do you have any ideas what this might look like? In express-fp (which uses import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as session from 'express-session';
import * as http from 'http';
import * as t from 'io-ts';
import {
BadRequest,
JsValue,
jsValueWriteable,
Ok,
Result,
} from 'express-result-types/target/result';
import { formatValidationErrors } from './helpers/other';
import { wrap } from './index';
const app = express();
app.use(session({ secret: 'foo' }));
// Don't parse body using middleware. Body parsing is instead handled in the request handler.
app.use(bodyParser.text({ type: 'application/json' }));
const Body = t.interface({
name: t.string,
});
const requestHandler = wrap(req => {
const jsonBody = req.body.asJson();
const maybeBody = jsonBody.chain(jsValue =>
jsValue.validate(Body).mapLeft(formatValidationErrors('body')),
);
return maybeBody
.map(body =>
Ok.apply(
new JsValue({
name: body.name,
}),
jsValueWriteable,
),
)
.getOrElse(error => BadRequest.apply(new JsValue(error), jsValueWriteable));
});
app.post('/', requestHandler);
const onListen = (server: http.Server) => {
const { port } = server.address();
console.log(`Server running on port ${port}`);
};
const httpServer = http.createServer(app);
httpServer.listen(8080, () => {
onListen(httpServer);
}); Usage:
Another common pattern is validating multiple things at once, such as the body and query params, and aggregating the errors. I have an example of this in express-fp over at https://github.com/OliverJAsh/express-fp/blob/07d0ee89d4a51c6d314e4c726e1ac551ce2a4a92/src/example.ts#L104. I would be very curious to see what these common patterns look like in hyper-ts. :-) |
I think you can even handle each route in a different way import * as express from 'express'
import * as bodyParser from 'body-parser'
import { status, closeHeaders, send, body } from 'hyper-ts/lib/MiddlewareTask'
import * as t from 'io-ts'
import { JSONFromString } from 'io-ts-types/lib/JSON/JSONFromString'
import { Either } from 'fp-ts/lib/Either'
import { Status } from 'hyper-ts'
const User = t.type({
name: t.string
})
type User = t.TypeOf<typeof User>
const userFromString = body(JSONFromString.pipe(User))
const userFromJson = body(User)
const badRequest = (message: string) =>
status(Status.BadRequest)
.ichain(() => closeHeaders)
.ichain(() => send(message))
const hello = (user: User) =>
status(200)
.ichain(() => closeHeaders)
.ichain(() => send(user.name))
const userHandler = (user: Either<t.Errors, User>) => user.fold(() => badRequest('invalid user'), hello)
const app = express()
// parse body as string
app.post('/a', bodyParser.text({ type: 'application/json' }), userFromString.ichain(userHandler).toRequestHandler())
// parse body as JSON
app.post('/b', bodyParser.json(), userFromJson.ichain(userHandler).toRequestHandler())
app.listen(3000, () => console.log('App listening on port 3000!')) |
It's definitely possible. import * as express from 'express'
import * as bodyParser from 'body-parser'
import { status, closeHeaders, send, body, param, query, MiddlewareTask } from 'hyper-ts/lib/MiddlewareTask'
import * as t from 'io-ts'
import { Status } from 'hyper-ts'
const User = t.type({
name: t.string
})
type User = t.TypeOf<typeof User>
const userId = param('user_id', t.string)
const userQuery = query(
t.type({
foo: t.string
})
)
const userBody = body(User)
const badRequest = (message: string) =>
status(Status.BadRequest)
.ichain(() => closeHeaders)
.ichain(() => send(message))
const map3 = <S, A, B, C>(
ma: MiddlewareTask<S, S, t.Validation<A>>,
mb: MiddlewareTask<S, S, t.Validation<B>>,
mc: MiddlewareTask<S, S, t.Validation<C>>
) => <R>(f: (a: A, b: B, c: C) => R): MiddlewareTask<S, S, t.Validation<R>> => {
return ma.ichain(ea => mb.ichain(eb => mc.map(ec => ea.chain(a => eb.chain(b => ec.map(c => f(a, b, c)))))))
}
const collect = map3(userId, userQuery, userBody)((id, query, body) => ({ id, query, user: body }))
const hello = (user: User) =>
status(200)
.ichain(() => closeHeaders)
.ichain(() => send(user.name))
const userHandler = collect.ichain(e => e.fold(() => badRequest('invalid user'), ({ user }) => hello(user)))
const app = express()
app.use(bodyParser.json())
app.post('/:user_id', userHandler.toRequestHandler())
app.listen(3000, () => console.log('App listening on port 3000!')) |
Hey @gcanti, thanks for the examples. Giving them a go now! I'm getting errors: const userFromString = body(JSONFromString.pipe(User))
Any ideas? |
@OliverJAsh In order to use
A possible quick solution would be to define a const MixedFromString: t.Type<t.mixed, t.mixed> = JSONFromString
const X = MixedFromString.pipe(User) |
Just coming back to this now, sorry it's taken so long! Could we define a |
In my original example with
Perhaps 2 can be solved by writing a new |
/cc @OliverJAsh