This lesson shows how to use the serbet
npm package to write a simple Node.js http server in ReasonML by implementing a simple "hello world" endpoint. Serbet is a convenient wrapper around Express.js. It's a young library, but its patterns are already used in production code.
Instructor: [0:00] Here I am all ready to make a server. Now to do this on top of Node.js and ReasonML, the first thing I'm going to do is install the server package. Npm install --save serbet, that's the name of the server package that we're going to use.
[0:16] I'm going to install it, full disclosure, I'm the author of this package. I've installed it. You can see here that it has some peer dependencies. Bs-platform, which we already have installed. It says that we don't because we're using 7, and I've listed 6 as the peer dependency for serbet.
[0:32] We see that it requires decco the package, and bs-let. The reason these two things aren't just included with the package itself, it's because it's intended that the consumer of the server will use these packages in their own files as well. Let's go ahead and install those. Npm install --save-dev decco bs-let.
[0:53] Now that those are installed we'll have to do our normal old bsconfig.json dance, and add to the bs-dependencies decco, because it has runtime dependencies, and serbet, because that contains our server modules. Then down in our ppx flag, let's add decco/ppx and also bs-let/ppx.
[1:12] The instructions for adding these things are in the ReadMe's for these packages, just so you know. I'll save the bsconfig, and I should be able to start using the serbet library right away. Here I'll make a module and I'll call it app = serbet.app, and I'll pass in an empty config.
[1:29] Oops, it looks like it can't find the server. This is a common problem, it probably means I need to restart my compiler over here on the right-hand side, now in other videos you may have seen not using a compiler in my terminal, that's because the editor does sometimes compile by itself.
[1:43] In practical day to day work, I almost always have the compiler running in a terminal over here so that I can see the output better. You'll see that now I've restarted the compiler, things are working out just fine. This might look strange to you, module App = serbet.App.
[1:55] Modules in Reason are much more flexible than what you might think coming from JavaScript. In JavaScript modules tend to be the files you put code in. In Reason they have namespaces, they can even be called like functions as you can see here. I'm constructing a module by passing information into a module constructor.
[2:14] You don't need to worry about that now too much, the OK community and the Reason community consider it an advanced topic, but it's really not that complex. All that you need to know now is you're making a module called app which has functions and values inside of it available to you, and you're doing that by calling serbet and passing any empty constructor.
[2:31] Now let's open that, open App. That will make all the values that inside app including all of the modules that are inside of it available for us to use inside the body of this file. Now let's make an endpoint on this app by typing module HelloEndpoint = App.handle.
[2:50] Now we're going to pass in a config to construct the handler this endpoint, or rather to construct the whole endpoint. This config expects some values that it's not getting right now. See how it says Modules do no match, empty module, but that's not included in handler config.
[3:05] It says that we need to drop a handler in there, a verb, path, etc., so let's start doing that. I have these memorized, so I don't really need to look at the docs, I'll just lead you through it. Let verb = GET; we're going to handle get, and the reason get is in scope is because we've opened App right here.
[3:22] Let path ="/hello/:name", you can see that I have a done a parameterized path there, we're going to be dealing with params too, in fact, let's make a type right now, @decco.decode, Decco is a language extension that automatically generates encoders and decoders for us.
[3:39] [@decco.decode] type params = { name: string}, and we'll use that type in just a moment, because the last thing we have to specify here is the handler function. Let handler = req, now this is a special function that takes in an express request and should return a promise of a response type, and that response type is specified inside of App, we'll show it to you in just a moment.
[4:03] The first thing that we're going to do is get the params off of the path. We do that by typing let%Async params = requireParams, you can see that's autocompleted there. The first thing we pass in is the decoder for the params, which is params_decode, that's automatically been generated for us by this language extension.
[4:24] It's the type name_decode, and then we pass in the request. Now what we'll have is the params properly parsed from the path. You might wonder why we're doing let%Async since dealing with params isn't exactly an Async operation.
[4:38] It's because we use promises as the vehicle to be able to validate params and if there's a problem with them, then we respond to the user. We shortcut automatically so that the programmer doesn't have to handle that, and we tell the user there was some problem with the prarams.
[4:50] This same pattern you'll say with requiring a body, or requiring query, basically it's just validating input. Now that that's done, we can actually use the params. Let's write a response. Here would type OKString, we're going to respond OK 200 with a string, and the string we're going to put in there is "Hello, " and then ++ the params.name ++ "!!!!!"
[5:14] Now this is still showing red, why is that? It's because it wants a promise and we've given it just a response value here. We need to wrap this inside of a promise, which we can do by using the Async module.Async function.
[5:30] Now if we open the Async module at the top of the file which is what I usually do, it'll look a little more natural here. There we can get rid of Async.async and just have OKString async. Now you see all my compiler errors have gone away, and it looks good to go.
[5:47] Let's try running it. Node src/Server.bs.js, oh, nothing happens. That's because I've registered the endpoint, but I haven't actually started the app listening yet. Let's do that. App.start and you can see here that it takes in an optional port, and then unit.
[6:04] Now it's important that you notice that unit is passed in at the end. You can see that port is optional, I don't actually have to pass it in. The only way that Reason knows that I'm done passing arguments into this function is here if I pass this unit in. If I don't want to pass in a port, I just pass in unit, we're good to go.
[6:22] Here I do want to pass in a port, so I'm going to specify that, port = 1337, and then don't forget to pass in unit, or else you'll see the same thing you saw over here. OK, let's try it again. Now you can see the server is listening on port 1337. Now let's try making a request.
Curl localhost: [6:39] 1337/hello/superface Hello, superface!!!!! There you have it. We've made a basic handler on top of Express running on Node, written with type safe Reason ML.