I'm having a bit of trouble understanding the proper way to use Ktor's DSL for request routing.
The problem is that when I test my API and try to GET /nomenclature/articles/categories
which should return a list of all article categories, I get instead Invalid article specified
which a message I return for the /nomenclature/articles/{articleId}
route when the articleId
parameter is invalid.
Here's my code:
route("/nomenclature") {
method(HttpMethod.Get) {
handle { call.respondText("The resource you accessed is not a valid REST resource, but a parent node. Children nodes include articles, categories, subcategories and so on.") }
}
route("articles") {
route("categories") {
get("{categoryId?}") {
val categoryIdParam = call.parameters["categoryId"]
if (categoryIdParam != null) {
val categoryId = categoryIdParam.toIntOrNull()
if (categoryId != null) {
val category = articlesDAO.findCategoryById(categoryId)
if (category != null) call.respond(category)
else call.respondText("Category not found", status = HttpStatusCode.NotFound)
} else call.respondText("Invalid category ID specified", status = HttpStatusCode.BadRequest)
} else {
val categories = articlesDAO.getCategories()
if (categories != null) call.respond(categories)
else call.respondText("No categories found", status = HttpStatusCode.NotFound)
}
}
}
route("subcategories") {
get("{subcategoryId?}") {
val subcategoryIdParam = call.parameters["subcategoryId"]
if (subcategoryIdParam != null) {
val subcategoryId = subcategoryIdParam.toIntOrNull()
if (subcategoryId != null) {
val subcategory = articlesDAO.findSubcategoryById(subcategoryId)
if (subcategory != null) call.respond(subcategory)
else call.respondText("Subcategory not found", status = HttpStatusCode.NotFound)
} else call.respondText("Invalid subcategory ID specified", status = HttpStatusCode.BadRequest)
} else {
val subcategories = articlesDAO.getCategories()
if (subcategories != null) call.respond(subcategories)
else call.respondText("No subcategories found", status = HttpStatusCode.NotFound)
}
}
}
get("types") {
val articleTypes = articlesDAO.getArticleTypes()
if (articleTypes != null) call.respond(articleTypes)
else call.respondText("No article types found", status = HttpStatusCode.NotFound)
}
get("wholePackagings") {
val wholePackagings = articlesDAO.getWholePackagings()
if (wholePackagings != null) call.respond(wholePackagings)
else call.respondText("No whole packagings found", status = HttpStatusCode.NotFound)
}
get("fractionsPackagings") {
val fractionsPackagings = articlesDAO.getFractionPackagings()
if (fractionsPackagings != null) call.respond(fractionsPackagings)
else call.respondText("No fractions packagings found", status = HttpStatusCode.NotFound)
}
get("{articleId?}") {
val articleIdParam = call.parameters["articleId"]
if (articleIdParam != null) {
val articleId = articleIdParam.toIntOrNull()
if (articleId != null) {
val article = articlesDAO.findArticleById(articleId)
if (article != null) call.respond(article) else call.respondText("No article found", status = HttpStatusCode.NotFound)
} else call.respondText("Invalid article ID specified", status = HttpStatusCode.BadRequest)
} else {
val articles = articlesDAO.getArticles()
if (articles != null) call.respond(articles) else call.respondText("No articles found", status = HttpStatusCode.NotFound)
}
}
}
}
It would be really great if someone could help me by providing a basic but somewhat comprehensive example of how to use all Ktor route matchers and handlers, including in a nested resource manner.
EDIT: I've rewritten the routing using a different approach but I would still like to know why my previous version didn't work as expected. Here's my second approach that seems to work as expected (I've tested it):
routing {
route("/v1") {
route("stock") {
get {
val stock = stockDAO.getAllStock()
if (stock != null) call.respond(stock) else call.respondText("No stock found", status = HttpStatusCode.NotFound)
}
get("{locationId}") {
val locationIdParam = call.parameters["locationId"]
if (locationIdParam != null) {
val locationId = locationIdParam.toIntOrNull()
if (locationId != null) {
val stock = stockDAO.getStockByLocationId(locationId)
if (stock != null) call.respond(stock) else call.respondText("No stock found", status = HttpStatusCode.NotFound)
} else call.respondText("ERROR: Invalid location ID specified.", status = HttpStatusCode.BadRequest)
}
}
}
route("nomenclature") {
get {
call.respondText("The resource you accessed is not a valid REST resource, but a parent node. Children nodes include articles, categories, subcategories and so on.")
}
route("articles") {
get {
val articles = articlesDAO.getArticles()
if (articles != null) call.respond(articles) else call.respondText("No articles found", status = HttpStatusCode.NotFound)
}
get("{articleId}") {
val articleIdParam = call.parameters["articleId"]
if (articleIdParam != null) {
val articleId = articleIdParam.toIntOrNull()
if (articleId != null) {
val article = articlesDAO.findArticleById(articleId)
if (article != null) call.respond(article) else call.respondText("No article found", status = HttpStatusCode.NotFound)
} else call.respondText("Invalid article ID specified", status = HttpStatusCode.BadRequest)
}
}
route("categories") {
get {
val categories = articlesDAO.getCategories()
if (categories != null) call.respond(categories)
else call.respondText("No categories found", status = HttpStatusCode.NotFound)
}
get("{categoryId}") {
val categoryIdParam = call.parameters["categoryId"]
if (categoryIdParam != null) {
val categoryId = categoryIdParam.toIntOrNull()
if (categoryId != null) {
val category = articlesDAO.findCategoryById(categoryId)
if (category != null) call.respond(category)
else call.respondText("Category not found", status = HttpStatusCode.NotFound)
} else call.respondText("Invalid category ID specified", status = HttpStatusCode.BadRequest)
}
}
}
route("subcategories") {
get {
val subcategories = articlesDAO.getSubcategories()
if (subcategories != null) call.respond(subcategories)
else call.respondText("No subcategories found", status = HttpStatusCode.NotFound)
}
get("{subcategoryId}") {
val subcategoryIdParam = call.parameters["subcategoryId"]
if (subcategoryIdParam != null) {
val subcategoryId = subcategoryIdParam.toIntOrNull()
if (subcategoryId != null) {
val subcategory = articlesDAO.findSubcategoryById(subcategoryId)
if (subcategory != null) call.respond(subcategory)
else call.respondText("Subcategory not found", status = HttpStatusCode.NotFound)
} else call.respondText("Invalid subcategory ID specified", status = HttpStatusCode.BadRequest)
}
}
}
get("types") {
val articleTypes = articlesDAO.getArticleTypes()
if (articleTypes != null) call.respond(articleTypes)
else call.respondText("No article types found", status = HttpStatusCode.NotFound)
}
get("wholePackagings") {
val wholePackagings = articlesDAO.getWholePackagings()
if (wholePackagings != null) call.respond(wholePackagings)
else call.respondText("No whole packagings found", status = HttpStatusCode.NotFound)
}
get("fractionsPackagings") {
val fractionsPackagings = articlesDAO.getFractionPackagings()
if (fractionsPackagings != null) call.respond(fractionsPackagings)
else call.respondText("No fractions packagings found", status = HttpStatusCode.NotFound)
}
}
}
}
}
The reason is that you haven't specified any handler for GET /nomenclature/articles/categories
.
You set up a route for /articles
, and then for /categories
, but there is no handler inside.
The only thing inside is get("{categoryId?}")
, which doesn't match, since there is no tailcard.
The reason for getting /nomenclature/articles/{articleId}
is, that it first tries to match /articles
, but since it cannot match /categories
(there is no handler), it goes on looking and finally finds the last route, which contains a wildcard parameter and matches.
If you want to set up a handler for that specific route, here is how:
route("articles") {
route("categories") {
get("{categoryId?}") {
...
}
get {
... your code ...
}
}
}