← All @molecule/* packages · App templates
@molecule/api-resource-bookmarkAPI resource · bookmarks · API (Node) · v1.0.1 · Apache-2.0
Bookmark/favorite any resource with folder organization
npm install @molecule/api-resource-bookmark@molecule/api-resource-bookmark is an API resource: the routes, validation and storage for bookmarks, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { routes, requestHandlerMap } from '@molecule/api-resource-bookmark'
// Wire routes into your Express app via mlcl inject
// POST /bookmarks
// GET /bookmarks
// GET /bookmarks/folders
// GET /bookmarks/check/:resourceType/:resourceId
// DELETE /bookmarks/:resourceType/:resourceIdAuto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Bookmark/favorite resource for molecule.dev.
Allows users to bookmark any resource, organize into folders, and check bookmark status.
import { routes, requestHandlerMap } from '@molecule/api-resource-bookmark'
// Wire routes into your Express app via mlcl inject
// POST /bookmarks
// GET /bookmarks
// GET /bookmarks/folders
// GET /bookmarks/check/:resourceType/:resourceId
// DELETE /bookmarks/:resourceType/:resourceId
resource
npm install @molecule/api-resource-bookmark @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource zod
BookmarkA bookmark linking a user to a resource, with optional folder grouping.
interface Bookmark {
/** Unique bookmark identifier. */
id: string
/** The ID of the user who created the bookmark. */
userId: string
/** The type of resource bookmarked (e.g. 'post', 'project'). */
resourceType: string
/** The ID of the bookmarked resource. */
resourceId: string
/** Optional folder name for organizing bookmarks. */
folder: string | null
/** When the bookmark was created (ISO 8601). */
createdAt: string
/** When the bookmark was last updated (ISO 8601). */
updatedAt: string
}
BookmarkQueryQuery options for listing bookmarks.
interface BookmarkQuery {
/** Filter by resource type. */
resourceType?: string
/** Filter by folder name. */
folder?: string
/** Maximum number of results to return. */
limit?: number
/** Number of results to skip. */
offset?: number
}
PaginatedResultA paginated result set.
interface PaginatedResult<T> {
/** The result items for the current page. */
data: T[]
/** Total number of matching items across all pages. */
total: number
/** Maximum number of results per page. */
limit: number
/** Number of results skipped. */
offset: number
}
addBookmark(userId, resourceType, resourceId, folder)Adds a bookmark. Idempotent — returns existing bookmark if already bookmarked.
function addBookmark(
userId: string,
resourceType: string,
resourceId: string,
folder?: string,
): Promise<Bookmark>
userId — The user ID.resourceType — The type of resource to bookmark.resourceId — The ID of the resource to bookmark.folder — Optional folder name.Returns: The created or existing bookmark.
check(req, res)Checks whether the current user has bookmarked a resource.
function check(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with resourceType and resourceId params.res — The response object.create(req, res)Adds a bookmark for the current user. Idempotent.
function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with bookmark body (resourceType, resourceId, folder?).res — The response object.del(req, res)Removes a bookmark by resource type and ID.
function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with resourceType and resourceId params.res — The response object.folders(_req, res)Lists all unique folder names for the current user's bookmarks.
function folders(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>
_req — The request (unused).res — The response object.getBookmarks(userId, options)Gets all bookmarks for a user with optional filtering and pagination.
function getBookmarks(userId: string, options?: BookmarkQuery): Promise<PaginatedResult<Bookmark>>
userId — The user ID.options — Query options.Returns: Paginated bookmarks.
getFolders(userId)Gets all unique folder names for a user's bookmarks.
function getFolders(userId: string): Promise<string[]>
userId — The user ID.Returns: Array of folder names.
isBookmarked(userId, resourceType, resourceId)Checks if a resource is bookmarked by a user.
function isBookmarked(userId: string, resourceType: string, resourceId: string): Promise<boolean>
userId — The user ID.resourceType — The type of resource.resourceId — The ID of the resource.Returns: true if bookmarked.
list(req, res)Lists the current user's bookmarks with optional filtering and pagination.
function list(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with optional query params (resourceType, folder, limit, offset).res — The response object.removeBookmark(userId, resourceType, resourceId)Removes a bookmark.
function removeBookmark(userId: string, resourceType: string, resourceId: string): Promise<void>
userId — The user ID.resourceType — The type of resource.resourceId — The ID of the resource.createBookmarkSchemaSchema for validating bookmark creation input.
const createBookmarkSchema: z.ZodObject<
{ resourceType: z.ZodString; resourceId: z.ZodString; folder: z.ZodOptional<z.ZodString> },
z.core.$strip
>
requestHandlerMapHandler map for bookmark routes.
const requestHandlerMap: {
readonly create: typeof create
readonly list: typeof list
readonly check: typeof check
readonly folders: typeof folders
readonly del: typeof del
}
routesRoutes for bookmark add/remove/list/check and folder listing.
const routes: readonly [
{
readonly method: 'post'
readonly path: '/bookmarks'
readonly handler: 'create'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/bookmarks'
readonly handler: 'list'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/bookmarks/folders'
readonly handler: 'folders'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/bookmarks/check/:resourceType/:resourceId'
readonly handler: 'check'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/bookmarks/:resourceType/:resourceId'
readonly handler: 'del'
readonly middlewares: readonly ['authenticate']
},
]
Peer dependencies:
@molecule/api-database ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-logger ^1.0.1@molecule/api-resource ^1.0.1zod ^4.0.0@molecule/api-database
@molecule/api-i18n
@molecule/api-logger
@molecule/api-resource
zod
List endpoints return a PAGINATED envelope { data, total, limit, offset }, not a
bare array — read the rows off result.data (server). On the client, unwrapList(res)
from @molecule/app-http normalizes this envelope (pass it the whole HttpResponse), so
the rows come back; reading the response as a bare array — or res.data alone (which is
the envelope) — yields an EMPTY list.
Migration required. src/__setup__/bookmarks.sql ships with this package
and must exist in the target database before use (scaffolded apps apply it
automatically). Note the UNIQUE ("userId","resourceType","resourceId")
constraint — one bookmark per user per resource.
addBookmark() is idempotent and does NOT move folders. Re-adding an
existing bookmark returns the existing row unchanged — to move a bookmark to
another folder, remove and re-add it (or add your own update path).
Owner-scoped via the session. All routes require authenticate and every
query filters by the session userId — never accept a target userId from the
client (IDOR).
Bookmarked resources are polymorphic and unverified (no FK): use the same
canonical resourceType slugs as your other polymorphic resources
(comments, activity feed) so check/remove keys line up.
Folders are free-form strings on the bookmark row (GET /bookmarks/folders
returns the distinct set) — there is no folder entity to create first.
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
folder
string, set at create time — re-adding does NOT move it between folders)
shows under that folder, GET /bookmarks/folders returns the distinct folder
set, and GET /bookmarks?folder=X (and ?resourceType=X) filters the list to
only the matching bookmarks.