Skip to main content

Resource-Based Routing in Alapa

Resource-based routing in Alapa provides a streamlined way to define CRUD (Create, Read, Update, Delete) operations for a resource. This approach automatically maps common actions to specific routes, reducing boilerplate code and ensuring consistency across your application.


Introduction to Resource-Based Routing

Resource-based routing is a convention-over-configuration approach that maps HTTP verbs to controller actions for a specific resource. For example, a Book resource might have routes for listing books, showing a specific book, creating a new book, updating a book, and deleting a book.

Alapa simplifies this process by automatically generating these routes when you define a resourceful controller.


Default Resourceful Routes

When you define a resourceful controller, Alapa generates the following routes by default:

HTTP VerbURLActionRoute NameDescription
GET/booksindexbooks.indexList all books
GET/books/createcreatebooks.createShow the form to create a new book
POST/booksstorebooks.storeStore a newly created book
GET/books/:bookshowbooks.showShow a specific book
GET/books/:book/editeditbooks.editShow the form to edit a book
PUT/PATCH/books/:bookupdatebooks.updateUpdate a specific book
DELETE/books/:bookdestroybooks.destroyDelete a specific book

Implementing a Resourceful Controller

To implement a resourceful controller, create a class that implements the ResourcefulController interface. Each method in the class corresponds to the actions.

Example: BookController

src/app/books/Controller.ts
import { ResourcefulController, Request, Response } from "alapa";
import { Books } from "../../models/book";

export class BooksController implements ResourcefulController {
// List all books
async index(req: Request, res: Response) {
const books = await Books.find();
res.render("books.index", { books });
}

// Show the form to create a new book (optional)
create(req: Request, res: Response) {
res.render("books.create");
}

// Store a newly created book
async store(req: Request, res: Response) {
const book = new Books();
book.title = req.body.title;
book.author = req.body.author;
await book.save();
res.navigate.back("success", "New Book Created Successfully");
}

// Show a specific book
async show(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
res.render("books.show", { book });
} else {
res.render("error.404", { message: "Book not found" });
}
}

// Show the form to edit a book (optional)
async edit(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
res.render("books.edit", { book });
}

// Update a specific book
async update(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
book.title = req.body.title;
book.author = req.body.author;
await book.save();
req.flash("success", "Book updated successfully");
} else {
req.flash("error", "Book not found");
}
res.navigate.back();
}

// Delete a specific book
async destroy(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
await book.delete();
req.flash("success", "Book deleted successfully");
} else {
req.flash("error", "Book not found");
}
res.navigate.route("books.index");
}
}

RestfulResource (or ApiResource)

For RESTful APIs, Alapa provides a specialized interface called RestfulResource (or ApiResource). This interface excludes methods like create and edit, which are typically used for rendering forms in web applications. Instead, it focuses on the core CRUD operations required for APIs.

Example: BookApiController

src/api/books/Controller.ts
import { RestfulResource, Request, Response } from "alapa";
import { Books } from "../../models/book";

export class BooksController implements RestfulResource {
// List all books
async index(req: Request, res: Response) {
const books = await Books.find();
res.json(books);
}

// Store a newly created book
async store(req: Request, res: Response) {
const book = new Books();
book.title = req.body.title;
book.author = req.body.author;
await book.save();
res.status(201).json(book);
}

// Show a specific book
async show(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
res.json(book);
} else {
res.status(404).json({ message: "Book not found" });
}
}

// Update a specific book
async update(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
book.title = req.body.title;
book.author = req.body.author;
await book.save();
res.json(book);
} else {
res.status(404).json({ message: "Book not found" });
}
}

// Delete a specific book
async destroy(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
await book.delete();
res.status(204).send();
} else {
res.status(404).json({ message: "Book not found" });
}
}
}

Registering RestfulResource Routes

src/app/api/books/router.ts
import { Router } from "alapa";
import { BooksController } from "./ApiController";

const bookRoutes = new Router();
bookRoutes.restResource("books", BookApiController);
// OR
bookRoutes.apiResource("books", BookApiController);
export default bookRoutes;

This will generate the following routes:

HTTP VerbURLActionRoute Name
GET/booksindexbooks.index
POST/booksstorebooks.store
GET/books/:bookshowbooks.show
PUT/PATCH/books/:bookupdatebooks.update
DELETE/books/:bookdestroybooks.destroy

Using the @Params Decorator

The @Params decorator is used to define dynamic segments in your routes. It allows you to specify the parameter names for resourceful routes.

Example: Using @Params

src/app/books/Controller.ts
import { ResourcefulController, Request, Response, Params } from "alapa";
import { Books } from "../../models/book";

export class BookController implements ResourcefulController {
// /books/:book/:author
@Params("author") // Define the parameter name
async show(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
res.render("books.show", { book });
} else {
res.navigate.back().withErrors("Book not found");
}
}
}

ResourcefulOptions Configuration

The ResourcefulOptions interface provides a wide range of options to customize resourceful routes. Below is a detailed breakdown of these options with examples:

changeNamesWithVerbs

  • Type: boolean
  • Default: true
  • Description: Whether to change route names to use the provided verb names.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
changeNamesWithVerbs: false, // Use the default route names
});

paramNames

  • Type: string
  • Description: Custom parameter names for dynamic segments in the route.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
paramNames: "id", // Use `id` instead of `book` in the route
});

docPrefix

  • Type: string
  • Description: Prefix added to the API documentation path.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
docPrefix: "api/v1", // Add a prefix to API documentation paths
});

createNames

  • Type: boolean
  • Default: true
  • Description: Whether to create route names for create actions.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
createNames: false, // Don't create route names
});

namePrefix

  • Type: string
  • Description: Prefix added to route names.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
namePrefix: "app-books", // Add a prefix to route names
});

only

  • Type: string[] | string
  • Description: Specify which routes to include.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
only: ["index", "show"], // Only include index and show routes
});

except

  • Type: string[] | string
  • Description: Specify which routes to exclude.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
except: ["create", "edit"], // Exclude create and edit routes
});

verb

  • Type: ResourcefulVerb
  • Description: Custom verb names for resourceful routes.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
verb: {
index: "list", // Use "list" instead of "index"
show: "detail", // Use "detail" instead of "show"
},
});

middleware

  • Type: ResourcefulMiddleware
  • Description: Middleware functions for resourceful routes.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
middleware: {
index: [authMiddleware], // Apply middleware to the index action
store: [logMiddleware], // Apply middleware to the store action
},
});

mergeMiddleware

  • Type: boolean
  • Default: false
  • Description: Whether to merge a single route middleware with the before middleware.

Example:

src/app/books/router.ts
import { BooksController } from "./ApiController";

bookRoutes.resource("books", BooksController, {
middleware: {
before: [authMiddleware], // Apply middleware before all actions
index: [logMiddleware], // Apply middleware to the index action
},
mergeMiddleware: true, // Merge middleware for index with before middleware
});

Best Practices

  1. Use Meaningful Resource Names: Choose resource names that clearly describe the entity (e.g., books, users).
  2. Keep Controllers Focused: Each controller should handle a single resource.
  3. Leverage Middleware: Use middleware for cross-cutting concerns like authentication and logging.
  4. Document Your API: Use OpenAPI decorators to document your routes.
  5. Test Your Routes: Write unit and integration tests for your resourceful routes.