conduit
command line tool by running the following command in your shell:heroes
by entering the following in your shell:heroes
project directory with a default server implementation populating it.http://localhost:8888
to fetch and manipulate hero data. The application you will build in this tutorial respond to those requests.GET /heroes
to the list of heroesGET /heroes/:id
to get an individual heroGET /heroes
is called an operation. It is the combination of the HTTP method and the path of the request. Each operation is unique to an application, so your code is segmented into units for each operation. Sections with a colon, like the ':id' segment, are variable: they can be 1, 2, 3, and so on.Router
that makes sure the request path is /heroes
or /heroes/:id
HeroesControllers
that responds with hero objectsApplicationChannel
that you override methods in to set up your controllers. This type is already declared in lib/channel.dart
- open this file and find ApplicationChannel.entryPoint
:entryPoint
controller is the first to handle it. In our case, this is a Router
- a subclass of Controller
.Controller
. There are some controller subclasses already in Conduit for common behaviors.route
method on a router to attach a controller to a route. A route is a string syntax that matches the path of a request. In our current implementation, the route will match every request with the path /example
. When that request is received, a linked function runs and returns a 200 OK response with an example JSON object body./heroes
to a controller of our own, so we can control what happens. Let's create a HeroesController
. Create a new file in lib/controller/heroes_controller.dart
and add the following code (you will need to create the subdirectory lib/controller/
):HeroesController
is a subclass of Controller
; this is what makes it a controller object. It overrides its handle
method by returning a Response
object. This response object has a 200 OK status code, and it body contains a JSON-encoded list of hero objects. When a controller returns a Response
object from its handle
method, it is sent to the client.HeroesController
isn't hooked up to the application channel. We need to link it to the router. First, import our new file at the top of channel.dart
.HeroesController
to the Router
for the path /heroes
:http://conduit-tutorial.conduit.dart.io
. It will make a request to http://localhost:8888/heroes
and your application will serve it. You'll see your heroes in your web browser:conduit serve
in.Router
will send a 404 Not Found response for any request. Adding a route to a Router
creates an entry point to a new channel that controllers can be linked to. In our application, HeroesController
is linked to the route /heroes
.HeroesController
, always send a response. They implement the behavior that a request is seeking. Middleware controllers, like Router
, handles requests before they reach an endpoint controller. A router, for example, handles a request by directing it to the right controller. Controllers like Authorizer
verify the authorization of the request. You can create all kinds of controllers to provide any behavior you like.Router
allows for many. For example, a larger application might look like this:Controller
, giving them the ability to be linked together to handle requests. A request goes through controllers in the order they are linked. A request for the path /users
will go through an APIKeyValidator
, an Authorizer
and finally a UsersController
. Each of these controllers has an opportunity to respond, preventing the next controller from receiving the request.GET /heroes
requests. The browser application uses the this list to populate its hero dashboard. If we click on an individual hero, the browser application will display an individual hero. When navigating to this page, the browser application makes a request to our server for an individual hero. This request contains the unique id of the selected hero in the path, e.g. /heroes/11
or /heroes/13
./heroes
. Since a request for individual heroes will have a path that changes depending on the hero, we need our route to include a path variable.:
). For example, the route /heroes/:id
contains a path variable named id
. If the request path is /heroes/1
, /heroes/2
, and so on, the request will be sent to our HeroesController
. The HeroesController
will have access to the value of the path variable to determine which hero to return./heroes/:id
no longer matches the path /heroes
. It'd be a lot easier to organize our code if both /heroes
and /heroes/:id
went to our HeroesController
; it does heroic stuff. For this reason, we can declare the :id
portion of our route to be optional by wrapping it in square brackets. In channel.dart
, modify the /heroes
route:/heroes
still matches the route. If the path contains a second segment, the value of that segment is bound to the path variable named id
. We can access path variables through the Request
object. In heroes_controller.dart
, modify handle
:conduit serve
again. In the browser application, click on a hero and you will be taken to a detail page for that hero.curl -X GET http://localhost:8888/heroes/11
to view the single hero object. You can also trigger a 404 Not Found response by getting a hero that doesn't exist.HeroesController
is OK right now, but it'll soon run into a problem: what happens when we want to create a new hero? Or update an existing hero's name? Our handle
method will start to get unmanageable, quickly.ResourceController
comes in. A ResourceController
allows you to create a distinct method for each operation that we can perform on our heroes. One method will handle getting a list of heroes, another will handle getting a single hero, and so on. Each method has an annotation that identifies the HTTP method and path variables the request must have to trigger it.heroes_controller.dart
, replace HeroesController
with the following:handle
in ResourceController
. A ResourceController
implements this method to call one of our operation methods. An operation method - like getAllHeroes
and getHeroByID
- must have an Operation
annotation. The named constructor Operation.get
means these methods get called when the request's method is GET. An operation method must also return a Future<Response>
.getHeroByID
's annotation also has an argument - the name of our path variable id
. If that path variable exists in the request's path, getHeroByID
will be called. If it doesn't exist, getAllHeroes
will be called.conduit serve
and then run conduit serve
again. The browser application should still behave the same.curl
, you can create a SwaggerUI browser application that executes requests against your locally running application. In your project directory, run conduit document client
and it will generate a file named client.html
. Open this file in your browser for a UI that constructs and executes requests that your application supports.getHeroByID
method, we make a dangerous assumption that the path variable 'id' can be parsed into an integer. If 'id' were something else, like a string, int.parse
would throw an exception. When exceptions are thrown in operation methods, the controller catches it and sends a 500 Server Error response. 500s are bad, they don't tell the client what's wrong. A 404 Not Found is a better response here, but writing the code to catch that exception and create this response is cumbersome.getHeroByID()
:id
will be parsed as an integer and be available to this method in the id
parameter. The @Bind
annotation on an operation method parameter tells Conduit the value from the request we want bound. Using the named constructor Bind.path
binds a path variable, and the name of that variable is indicated in the argument to this constructor.@Bind.path(pathVariableName)
.@Bind.path('id') int heroID
. Only the argument to Bind
's constructor must match the actual name of the path variable. This is valuable for other types of bindings, like headers, that may contain characters that aren't valid Dart variable names, e.g. X-API-Key
.