This workshop will be retired on May 31, 2020.
Refactoring Routes Using Controllers13:02 with Pasan Premaratne
Our routes are all implemented inside the routes function and it's very hard to understand what our code does. In this final video, let's refactor our code to use Vapor's controllers.
We implemented all the functionality we outlined at the start of this series, 0:00 but our code could use a refactor. 0:04 All our routes are just nested calls in a single route's function. 0:07 And it's hard to distinguish what does what without reading all of the comments. 0:11 So let's make use of Vapor's controllers to clean things up. 0:16 If you're in code we're going to close this out, and then back in our terminal. 0:19 We're going to create the files at a command line and 0:25 regenerate the xcode project. 0:27 If you're in a regular text editor, you can keep it open while you do this. 0:29 So navigate to the root directory of the project and run the following commands. 0:33 So touch Sources/App/Controllers/CoursesController- 0:37 .swift. 0:42 And then, touch 0:43 Sources/App/Controllers/ReviewsController- .swift. 0:45 If you're using xcode for this project, 0:54 follow that up with the command to regenerate the xcode project. 0:56 So Vapor xcode -y, okay, back in xcode, at least on my end, 0:59 let's navigate to the new courses controller swift file. 1:05 We're going to create a controller to handle a subset of the routes. 1:10 Controllers in Vapor, let's go there, so coursesC. 1:15 Controllers in vapor define a set of routes which are then registered with 1:21 the router. 1:25 This allows us to group all related routes into a single object. 1:26 We'll start by importing Vapor and Fluent. 1:30 Next, we're going to create a struct named CoursesController, 1:36 which conforms to a RouteCollection protocol. 1:41 If you're coming from iOS, a struct as a controller might seem odd. 1:44 Since we're used to controllers being these heavy, 1:48 stateful objects that do a bunch of stuff. 1:50 Our controller here is simply going to define a bunch of functions that handle 1:53 various routes. 1:56 The RouteCollection protocol groups a collection of routes to add to a route, 1:58 and has a single requirement, a boot function. 2:03 This boot function includes a router instance as an argument, and 2:07 we can use these to build our routes. 2:10 In fact, you can cut and paste. 2:13 So if we go to routes.swift, we can cut and 2:15 paste all of the course related routes starting over here and 2:19 all the way to the top, and get rid of that. 2:24 And then go back to the CoursesController, and paste that in, and 2:30 you won't have to make any changes. 2:34 When you do that though, the router doesn't know that these routes exist 2:37 anymore, because all routes are registered through the routes function. 2:40 To fix that, we need to go back to routes.swift and 2:44 register a route collection. 2:47 And all this requires is creating an instance of the course controller and 2:50 registering it. 2:54 So we'll do that inside the body of the routes function. 2:55 We'll say, let coursesController = CoursesController. 2:57 And then, we can say try router.register a collection, 3:04 and we'll pass in the controller instance here. 3:09 If you build and run and rerun the course route collection in postman, 3:13 you'll see that everything still works. 3:17 So that was a quick and easy way to clean things up, but 3:19 there's more that we can do. 3:23 Let's navigate to CoursesControl.swift. 3:25 And right now, we're defining each route as a method core to the router. 3:27 The router instance here followed by a call back function. 3:33 And there's a tonne of duplication here as well. 3:37 We're writing out api and then courses in every single route and method signature. 3:39 So let's create a route group and simplify this. 3:45 So over the top we'll say, let the coursesRoutes = 3:48 router.grouped, And then we'll include that path component. 3:52 That every route contains. 3:59 So here we're asking the router to grade a group that contains api/courses 4:01 in the URL. 4:06 The grouped method returns a router object, and 4:07 we can use this new router object now instead of this base router in our routes. 4:11 For example, our first POST request or first POST route is simply 4:15 a call to /api/courses which is what this group represents. 4:20 So, we can replace router.post and all of this stuff with a POST 4:26 call to coursesRoutes.post, and that's it. 4:32 In fact, we can make this change everywhere, where we have api and 4:37 courses listed as a path. 4:41 So this is a get request, so we'll say coursesRoutes.get. 4:43 Now, the second get request includes a third path component, 4:49 the Course.parameter. 4:52 So this is going to become, we'll get rid of this part and 4:54 we'll say coursesRoutes.getCourse.parameter. 4:57 We use the same route for the next two methods 5:02 with different method calls to reflect the different HTTP verb so let's change that. 5:06 You can actually just select this, paste it in, and change this. 5:11 Do I put request, and then the last one will change that to a delete call. 5:16 Once you've made these changes verify that everything works by saving and 5:24 running the postman collection again. 5:28 Remember, that you will need to build and run the server again. 5:30 So that's cool, we got rid of a bunch of redundancy by using a route group. 5:33 But we can improve quite a bit more here, the logic for 5:38 all of these routes lives in the callback closures defined on each handler. 5:41 Aside from our comments and the method that we called on the router, 5:45 there isn't much else to indicate what each route is doing. 5:49 We can write each callback as a function, 5:53 which is then provided to the router as an argument. 5:56 Since we can name our functions, this makes our routes more readable. 6:00 Okay, let's try that out. 6:03 So, for the POST route, let's create a function that accepts a request and 6:06 returns a Future<Course. 6:11 So down here, outside of the boot function, 6:13 I'll say func createCourseHandler, and 6:18 this function excepts a request object. 6:23 And as the return type, so this is going to be a throwing function, and 6:27 as the return type it returns a Future<course>. 6:31 Notice that this is the exact same method signature as the closure. 6:34 For example, over here, they accept the request, return a Future<Course. 6:38 Now for the body, we can then paste the body of this callback. 6:42 So here we'll grab this, and then go down, and paste it in here. 6:47 All we have to do now is to ask the particular route 6:52 to this use function to handle the incoming request. 6:56 So we can go back to the top, where we've defined our host route, and we can say, 6:59 coursesRoutes.post (use: createCourseHandler). 7:04 And we just have to pass in the function as, or the method as a name, 7:12 we're not going to include any arguments. 7:17 The route will handle that when it's invoked. 7:19 Cool, so let's do this for all the routes. 7:22 I'm going to copy paste the get part, get rid of it. 7:25 We'll go down here and we'll say, 7:28 func getAllCoursesHandler it's a Request object. 7:32 And returns a Future<[Courses], 7:38 then we can paste this in, we'll go up to the top now. 7:43 And we can replace this by simply saying, 7:50 use: getAllCoursesHandler. 7:55 Oops, spelled that wrong handler, spelled wrong down here rather. 7:59 Handler, there we go. 8:04 Note, by the way, 8:06 that you pass the function in as an argument again without calling it. 8:07 For the next single course get request, let's say, 8:12 func getCourseHandler(_req: Request ). 8:18 Returns a single Course encapsulated in a Future, and you'll say, 8:23 I'll write out quickly return try req.parameters.next(Course.self). 8:28 All right, oops, what's going on here? 8:35 This needs to be throwing, they all do actually. 8:36 All right, and now we can go back up to the top here. 8:40 And we'll get rid of this, and we'll add a second argument 8:43 to this function to say, use: getCourseHandler. 8:48 Is there an error down there? 8:52 Throw rows, oops, we're getting like basic swift, throws, okay. 8:55 All right, so we have two more. 9:03 We'll call the next one updateCourseHandler. 9:05 Again, accepts our request and 9:10 it's a throwing function and this is going to turn a Future<Course. 9:14 And we'll grab the body of the put request here, 9:20 Put it in there, Make sure it runs and then up at the top. 9:27 Again, as a second argument we'll say, use: updateCourseHandler. 9:34 For the last rout, remember that a delete operation returns 9:38 a Future HTTPStatus, and not a Course. 9:42 So for our last function down here we'll say, 9:47 func deleteCourseHandler(_ req: Request, 9:52 throws Future <HTTPStatus. 9:58 And again we can simply just copy and paste this and 10:02 while I'm up here, we'll get rid of all this. 10:07 And we'll say, use: deleteCourseHandler. 10:11 Okay, and we'l paste that in. 10:16 Our boot function looks much cleaner now. 10:19 So if we go back up to the top, we don't need these comments, 10:21 in fact, because the method names are pretty descriptive. 10:24 So here we'll say, get rid of all this 10:28 There we go, the boot function is much cleaner. 10:37 We can actually make one more change. 10:38 And this is a bit of Vapor magic. 10:40 So up to here I think these are objectively good ways to structure your 10:42 routes, around groups and your collections. 10:45 Now the next bit is subjective, it's up to you if you want to implement this. 10:47 So let's go back to this POST route. 10:51 In the body of the create course handler, 10:54 we're decoding the json in the request into a course instance and saving it. 10:57 Vapor actually has some built in logic to handle this, and 11:02 we don't have to explicitly do this. 11:04 So we're gonna go down to the createCourseHandler function. 11:07 And we're gonna modify the argument list on the function to accept an instance of 11:11 course. 11:15 Next in the bunny, get rid of everything except this return. 11:17 This return call to the save, and the save operation, right? 11:22 So just that, your function should not look like this. 11:26 Now this breaks the POST rout handler and to fix it, 11:29 we need to include as a first argument, the type to decode. 11:33 So we're going to say Course.self. 11:37 Note, that it's Course.self here in contrast to Course.parameter 11:40 everywhere else. 11:44 And this is what tells Vapor that this is a type that we want to decode from 11:45 the request body compared to Course.parameter which indicates that it's 11:50 included as a path component. 11:54 So Vapor infers everything else here and our logic works all the same. 11:57 Don't trust me, though, double-check by running through the routes. 12:02 Okay, and that brings us to the end of this workshop. 12:05 There's a lot more to building a robust API, but you should have a handle now 12:09 on how to get something up and running quickly with Vapor. 12:13 The goal of this workshop was to give you a feel of what the Vapor API 12:16 feels like to work with. 12:20 I know that feels like isn't a good metric by any means, and 12:22 there's a lot we can actually talk about. 12:25 But when you're approaching a new framework, 12:27 often times it helps to see what it's like to work with it. 12:29 Secondly, this workshop should serve as a point of comparison. 12:32 We'll try our best to replicate this workshop across 12:36 all popular service site swift frameworks. 12:38 And in taking the content, you should have a good idea of what it's like to work 12:41 in all of them on a single example. 12:45 So going back to our app for a second, the review routes in the routes.swift 12:47 function, or rather, the routes.swift file, still needs cleaning up, but 12:52 I'm gonna leave that task up to you. 12:57 Have fun, and I hope you enjoyed this lightning introduction to Vapor. 12:59
You need to sign up for Treehouse in order to download course files.Sign up