Routing HTTP requests in a Dart server

Suragch
Suragch
Jan 2 · 9 min read

Part 2 in a series about Server Side Dart without a framework

original photo source

This article is a continuation in exploring Dart as an HTTP server backend, but without any frameworks or third-party packages. Knowing how to build a Dart server from scratch will not only make you a better developer in general, but will also keep you from being left in the lurch when your favorite framework falls off a cliff.

Go back and read Part 1: Building a Dart server from scratch if you haven’t already. And if you’re following along with the code examples (as you should be), then start with the final project code from that article.

HTTP server requests

GET     /posts
GET /posts/1
GET /posts/1/comments
GET /comments?postId=1
GET /posts?userId=1
POST /posts
PUT /posts/1
PATCH /posts/1
DELETE /posts/1

The left-hand side has the HTTP method, or verb as it’s sometimes called. The right-hand side has the path and various query parameters. These methods, paths, and query parameters are all ways of asking the server to do something specific, almost like function calls in a program.

In fact, if the HTTP requests above were rewritten as Dart functions, they might look something like this:

List<String> getPosts({int userId});
String getPost(int postId);
List<String> getComments(int postId);
void postPost(String content);
void putPost(int postId, String content);
void patchPost(int postId, String content);
void deletePost(int postId);

So basically, when you’re building a Dart server, you just need to convert the incoming HTTP requests into Dart functions. To do that you route the incoming request to a handling function based on the method and path of the request.

Routing requests

Let’s look at how to actually implement this in a Dart server now.

Routing based on method type

Future<void> main() async {
final server = await createServer();
print('Server started: ${server.address} port ${server.port}');
await handleRequests(server);
}
Future<void> handleRequests(HttpServer server) async {
await for (HttpRequest request in server) {
switch (request.method) {
case 'GET':
handleGet(request);
break;
case 'POST':
handlePost(request);
break;
default:
handleDefault(request);
}
}
}

HttpRequest.method is a string in the form of GET, POST, PUT, etc. The switch statement in the handleRequests function routes the request on to the appropriate Dart function. If you recall from the last lesson, when a request comes in that you don’t handle, then you should just return a 405 Method Not Allowed status.

That takes care of routing HTTP methods. Now let’s look at routing based on the path.

Routing based on path

Uri uri = request.uri;

Before implementing the path routing, it would be helpful to know the parts of a URI.

Understanding the components of a URI

http://www.example.com

However, it can contain a lot more components. This is also a URI:

http://user@www.example.com:8080/fruit/bananas?q=ripe

The Uri class in Dart gives you access to each of the components:

final uri = Uri.parse('http://user@www.example.com:8080/fruit/bananas?q=ripe');print(uri.scheme);    // http
print(uri.userInfo); // user
print(uri.host); // www.example.com
print(uri.port); // 8080
print(uri.path); // /fruit/bananas
print(uri.query); // q=ripe

I describe this in more detail in the article Understanding the Uri class in Dart (from examples).

Implementing path routing

var myStringStorage = 'Hello from a Dart server';void handleGet(HttpRequest request) {
request.response
..write(myStringStorage)
..close();
}

That doesn’t route anything. It just responds to every GET request the same way, no matter the path.

Replace handleGet with the following code:

void handleGet(HttpRequest request) {
final path = request.uri.path;
switch (path) {
case '/fruit':
handleGetFruit(request);
break;
case '/vegetables':
handleGetVegetables(request);
break;
default:
handleGetOther(request);
}
}

Now you’re routing the /fruit path one way, the /vegetables path another way, and everything else yet another way.

Add the handleGetFruit and handleGetVegetables functions:

void handleGetFruit(HttpRequest request) {
request.response
..statusCode = HttpStatus.ok
..write('banana')
..close();
}
void handleGetVegetables(HttpRequest request) {
request.response
..statusCode = HttpStatus.ok
..write(myStringStorage)
..close();
}

Both of these methods set the status code to 200 OK (which is the default so not technically required). handleGetFruit writes a static string to the response, but handleGetVegetables writes the contents of the myStringStorage variable.

Now handle any other GET requests by adding the handleGetOther function:

void handleGetOther(HttpRequest request) {
request.response
..statusCode = HttpStatus.badRequest
..close();
}

This time a status code of 400 Bad Request is returned with no further action. That means that anything that didn’t match the /fruit and /vegetables routes will automatically be rejected.

Note: Returning a 404 Not Found is another option and might be better. For a web page that would certainly make sense. I was thinking Bad Request because if this were a backend API for a Flutter app, anything besides /fruit and /vegetables is not even defined in the API. But let me know your opinion in the comments if you think otherwise.

Testing it out

You should see this response:

However, if you change the path to something like this:

You’ll see something like the following sad result:

Paths with multiple segments

GET /fruit
GET /vegetables
POST /vegetables

it would be better to have:

GET /v1/fruit
GET /v1/vegetables
POST /v1/vegetables

That way when you make a breaking change to the API, you can use a new version and allow the old one to serve those clients who haven’t upgraded yet:

GET /v2/fruits
GET /v2/veggies
POST /v2/veggies

In this case, though, it would be nice to separate the version from the rest of the path. The Uri class has a method for this too!

http://www.example.com/v1/fruitList<String> pathSegments = uri.pathSegments;
print(pathSegments); // [v1, fruit]

Note that the segments don’t start with / anymore.

This would allow you to route based on API version as well, perhaps something like the following:

final pathSegments = request.uri.pathSegments;
final version = pathSegments.first;
switch (version) {
case 'v1':
handleVersion1(request);
break;
case 'v2':
handleVersion2(request);
break;
default:
handleUnknownVersion(request);
}

Sometimes I also prefix paths with /api to differentiate the API from other resources on the server, something like /api/v1/fruit. In that case the version number would be located in pathSegments[1].

We’re not doing any versioning today, though, so let’s go on.

Query parameters

That’s where query parameters come in. You can use them to filter down or select single elements from a resource.

Selecting a single item

GET /posts/1

The server could define this to mean that it needs to return the all posts written by the user who has an ID of 1. In this case you could retrieve the user ID in a similar way to how you got the version number above:

List<String> pathSegments = request.uri.pathSegments;
final path = pathSegments[0]; // posts
final userId = pathSegments[1]; // 1

Selecting multiple items

GET /posts?userId=1

You saw earlier that Uri.query returns the query portion but even userId=1 is hard to work with. Since queries are key-value pairs, it would be nicer to get them as a map. Well, Dart has you covered with Uri.queryParameters!

http://www.example.com/fruit?color=yellow&size=very%20bigMap<String, String> queryParameters = uri.queryParameters;
print(queryParameters); // {color: yellow, size: very big}

Note that the URI query string is URL encoded, but is decoded in the map.

Let’s update our server to handle query parameters. First, add the following global constant:

const fruit = ['apple', 'banana', 'peach', 'pear'];

Then replace the handleGetFruit function with the following code:

void handleGetFruit(HttpRequest request) {  // 1
final queryParams = request.uri.queryParameters;
final prefix = queryParams['prefix'];

// 2
final matches = fruit
.where(
(item) => item.startsWith(prefix),
).toList();
// 3
if (matches.isEmpty) {
request.response
..statusCode = HttpStatus.notFound
..close();
// 4
} else {
final jsonString = jsonEncode(matches);
request.response
..statusCode = HttpStatus.ok
..write(jsonString)
..close();
}
}

Here’s what’s happening:

  1. You extract the key named prefix from the query parameters.
  2. Then you check the fruit list to see if any fruit starts with the value for prefix.
  3. If there’s no match then you return a 404 Not Found status code.
  4. But if there is a match then you encode it as a JSON string and send it back in the response. The dart:convert library that you imported in the last lesson for UTF8 decoding also includes jsonEncode, so that’s where that came from.

Testing it out

You should see something like this:

The server just returned the fruits that started with pe.

However, if you change the address to the following,

you’ll get a 404:

Normally you probably think of 404s as a bad thing, but in this case it shows that your server is working correctly!

Conclusion

Here are the key takeaways:

  • Use HttpRequest.method to route requests based on the HTTP method type, such as GET, POST, or PUT. If you don’t handle the requested method, then return a 405 Method Not Allowed.
  • Use HttpRequest.uri to get the URI. From there you can use Uri.path to route based on the entire path or Uri.pathSegments to create sub-routes based on path segments.
  • It’s a good idea to version your API with something like v1 and v2 in the path. In any case, be careful about making breaking changes to your API.
  • One way to select an individual element from a resource is to add an ID to your path, which you can extract with Uri.pathSegments.
  • Another way to select a single resource item, or to return a filtered list, is to use query parameters. You can extract these from a URI with Uri.queryParameters, which returns a Dart map.

As before, there is still a lot more work that you can do on this server. For example, you may have noticed the following GET request will crash your server now:

How would you fix that?

I added my solution to the Full code section below. However, I didn’t fix the crash you’d get when a query parameter is included but not supported:

Should you return a 404 Not Found or a 405 Bad Request? I’ll leave that one to you.

One of the rules to building any sort of application is to never trust user input. That includes HTTP requests. Unit testing is an important way to ensure that you are handling as many possible scenarios as you can think of. This prevents crashes and increases security. It’s also the next article I plan to write in this series.

Update: here it is:

Going on

Full code

🔗 Follow Flutter Community on Twitter: https://www.twitter.com/FlutterComm

Flutter Community

Articles and Stories from the Flutter Community

Suragch

Written by

Suragch

A Flutter and Dart developer. Follow me on Twitter @suragch1 to get updates of new articles.

Flutter Community

Articles and Stories from the Flutter Community

Suragch

Written by

Suragch

A Flutter and Dart developer. Follow me on Twitter @suragch1 to get updates of new articles.

Flutter Community

Articles and Stories from the Flutter Community

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store