10. Add an API endpoint¶
This page walks through a complete feature slice: a new operation in the contract, its controller and service, a test, the client method, and the documentation. The example adds GET /labels/{collectionId}/usage, which returns how many POAM records carry each label in a collection. Replace the names with your own.
10.1. Files impacted¶
File |
Change |
|---|---|
|
The new path item or operation. |
|
One exported handler named after the |
|
The service function with the access check and the SQL. |
|
Only when the schema changes. |
|
A test of the service logic. |
|
The client method. |
|
The client method’s spec. |
A component and its spec |
Whatever displays the result. |
|
The user-facing description, if the feature is visible. |
The commit subject |
|
10.2. Steps¶
Design the operation. Decide the path and method, the scope (
c-pat:readfor a read), whether it is administrative (then it takes elevate), the request and response schemas, and which of400,403, and404can occur. Look for an existing operation under the same tag and copy its structure; consistency with its neighbours matters more than novelty.Add it to the contract. Under
pathsinapi/specification/C-PAT.yaml:/labels/{collectionId}/usage: parameters: - $ref: '#/components/parameters/collectionIdPath' get: summary: Return the number of POAMs that carry each label in a collection operationId: getLabelUsage tags: - Label security: - oauth: - 'c-pat:read' responses: '200': description: Label usage counts content: application/json: schema: type: array items: type: object properties: labelId: type: integer labelName: type: string poamCount: type: integer '403': $ref: '#/components/responses/forbidden' default: $ref: '#/components/responses/unexpectedError'
tagsnames the controller file andoperationIdnames the export; both must match exactly. API contract and reference lists the other conventions.Lint the contract.
cd api npm run lint:spec
Add the controller export. In
api/Controllers/Label.js, next to the existing handlers:module.exports.getLabelUsage = async function getLabelUsage(req, res) { try { const usage = await labelService.getLabelUsage(req); res.status(200).json(usage); } catch (error) { sendError(res, error); } };
Write the service function. In
api/Services/labelService.js, using the file’swithConnectionhelper, the access helpers, and parameterized SQL:const { assertCollectionAccessLevel, READ_ACCESS_LEVEL } = require('./poamAccess'); module.exports.getLabelUsage = async function getLabelUsage(req) { const collectionId = Number.parseInt(req.params.collectionId, 10); return withConnection(async connection => { await assertCollectionAccessLevel(connection, req, collectionId, READ_ACCESS_LEVEL, 'You do not have access to this collection'); const sql = `SELECT l.labelId, l.labelName, COUNT(pl.poamId) AS poamCount FROM ${config.database.schema}.label l LEFT JOIN ${config.database.schema}.poamlabels pl ON pl.labelId = l.labelId WHERE l.collectionId = ? GROUP BY l.labelId, l.labelName`; const [rows] = await connection.query(sql, [collectionId]); return rows; }); };
For a lookup of one record, throw
new SmError.NotFoundError('Label not found')when the row is missing. For writes that touch more than one statement, usedbUtils.withTransactioninstead ofwithConnection. See Backend guide.Add a migration if the schema changes. Follow Add a database migration. This example needs none.
Run it. Start the API from
api/withnpm start. If the export name and theoperationIddisagree, startup fails withCould not find a [getLabelUsage] function in .... Open Swagger UI, authorize, and call the operation. Start the API withCPAT_DEV_RESPONSE_VALIDATION=logOnlyand check the log for aresponseValidationrecord; if one appears, the response and the schema disagree.Test the service. Create
api/test/labelService.test.jswithnode:testand a fake connection, asapi/test/poamAccess.test.jsdoes, and assert both the SQL parameters and the result. Run it withnode --test test/labelService.test.js. See Testing.Add the client method. In
client/src/app/pages/labels/labels.service.ts, following the file’s existing methods:getLabelUsage(collectionId: number): Observable<LabelUsage[]> { return this.http.get<LabelUsage[]>(`${this.cpatApiBase}/labels/${collectionId}/usage`).pipe(catchError(this.handleError)); }
Calls to C-PAT’s own API are never cached. If the operation you added is a
/tenable/proxy operation, add it toIntegrationServicewith auseCacheparameter instead; see Frontend guide.Test the client method. In
labels.service.spec.ts, using the HTTP testing controller the file already sets up:it('should load label usage for a collection', () => { service.getLabelUsage(1).subscribe((usage) => expect(usage).toEqual([])); const req = httpMock.expectOne(`${apiBase}/labels/1/usage`); expect(req.request.method).toBe('GET'); req.flush([]); });
Display it. Add or change the component that shows the result, with a signal for the data and a spec built from the mock factories. Add a client page covers a new page; for an existing one, follow the component’s current pattern.
Document and commit. Describe the visible behaviour in the user or admin guide, run the pre-submission checklist from Coding standards, and commit with a
feat:subject.