# Week 4: REST APIs, layers, and tests

Start with this: a client asks for something, the server does the work, and the
server sends an answer. In this project, the thing is a product stored in MongoDB.
The client could be Postman, a browser app, or one of our tests.

The [lecture and Now You Try instructions](https://si679-public.github.io/weeks/week04-rest/week04-notes)
are the source for this exercise. We built the lecture's GET and POST routes,
GET by id for Now You Try #1, its tests for #2, and both DELETE stretch items.

## 1. What an API request contains

An API is an agreed way for one program to ask another program to do something.
Here the agreement uses HTTP. A request has a method and a path, and sometimes a
body. For example:

```text
GET /products
```

`GET` means read. `/products` names the collection of products. The server answers
with a status code and a JSON body. JSON is text representing objects and arrays.
It looks like JavaScript, but keys must have double quotes.

```json
[
  {
    "id": "507f1f77bcf86cd799439011",
    "modelName": "Coffee maker",
    "modelNumber": "CM1",
    "manufacturer": "Acme",
    "color": "Black",
    "price": 49.99,
    "quantity": 10
  }
]
```

The outer square brackets mean an array. Each object is one product. That id is
an illustration; use an id from your own response when making a request.

## 2. What REST adds

REST gives us principles for keeping the client/server agreement consistent.
Use resource names such as `/products` and let the method describe the action.

| Method and path | Meaning | Success code |
| --- | --- | --- |
| `GET /products` | Read the collection | `200` |
| `GET /products/:id` | Read one product | `200` |
| `POST /products` | Create a product | `201` |
| `DELETE /products/:id` | Remove a product | `204` |

`:id` is a placeholder in our route definition. A real client replaces it with
the product's id. `GET /products/507f1f77bcf86cd799439011` is a concrete request.

`200` means success with an answer. `201` means created. `204` means success with
no response body. `400` means the request is malformed. `404` means the resource
or route does not exist. `500` means an unexpected server failure.

REST also says requests should be stateless: the request supplies what is needed
to handle it. The server does not need to remember which product you asked for
last time. This does **not** mean the database cannot store products between requests.

The client receives a representation of a product: a JSON copy of its current
fields. It does not receive direct control of the database document. If it changes
its local copy, the stored product does not change.

The lecture's six constraints are a uniform interface, client/server separation,
stateless requests, cacheability, a layered system, and optional code on demand.
Our exercise concentrates on naming, HTTP methods, representations, and layers.
It does not implement every feature of a complete shop API.

## 3. Why split the code into layers?

We could put a database query inside every route. It works initially, but then
database details, HTTP details, and application decisions are mixed together.
This week separates those jobs:

```text
client request
    -> app -> route -> controller -> service -> database
                             <- product <- document
client response <- controller
```

The model helps the service turn the document into a product. These are folders
inside one server program, not separate servers.

| File | Its job |
| --- | --- |
| `src/index.ts` | Connect to MongoDB, then start listening |
| `src/app.ts` | Assemble Express and middleware |
| `src/routes/product-routes.ts` | Match methods/paths to controllers |
| `src/controllers/product-controllers.ts` | Read the request and send the HTTP response |
| `src/services/product-service.ts` | Coordinate product operations and conversion |
| `src/models/product.ts` | Describe the Product type and convert fields/documents |
| `src/db/db.ts` | Connect and run MongoDB operations |
| `src/middleware/validate-id.ts` | Check the path id before using it |
| `src/middleware/validate-product.ts` | Check supplied product fields |
| `src/middleware/error-handler.ts` | Send a response for unexpected errors |
| `src/__tests__/products.test.ts` | Check behavior against a temporary database |

The route knows about the controller. The controller knows about the service.
The service knows about models and the database. The database does not know
about Express. That direction matters: changing the HTTP response should not
require changing a Mongo query.

## 4. Follow GET for one product

Open `product-routes.ts`. This line registers the route:

```ts
productRouter.get('/:id', validateId, productControllers.getProduct);
```

`app.ts` mounts this router at `/products`, so the complete path is `/products/:id`.
`validateId` runs first. A Mongo ObjectId is represented here by 24 hexadecimal
characters (digits and letters a–f). An invalid shape receives `400`.

The controller reads `req.params.id`. `params` is where Express puts values
matched by path placeholders. The controller calls:

```ts
const product = await productService.get(String(req.params.id));
```

The service calls `db.getInCollection(db.PRODUCTS, id)`. The database converts
the string to an `ObjectId` and searches for a document whose `_id` matches:

```ts
findOne({ _id: new ObjectId(id) })
```

If nothing matches, Mongo returns `null`. The service returns `null` too. The
controller turns that into `404`. The service does not choose `404`, because
that is an HTTP decision.

If a document exists, `productFromDocument` creates a new object containing
only the product fields. Mongo's `_id` becomes a string named `id`. The controller
calls `res.json(product)`, which sends that object with status `200`.

## 5. Read the TypeScript without getting stuck

```ts
const get = async (id: string): Promise<Product | null> => {
```

Read it as: "get takes a string id, does asynchronous work, and eventually gives
back a Product or null."

- `const` declares a name that cannot be reassigned.
- `async` means the function returns a Promise: an eventual result.
- `await` waits for that eventual result before continuing this function.
- `Product | null` means either a Product or no matching product.
- `Product[]` means an array of Products.
- `Promise<void>` means asynchronous work with no useful return value.

`type Product` describes an object's fields for TypeScript. It helps the compiler
catch mistakes; it does not check incoming JSON at runtime. Middleware does that.
`import type` brings in a type for the compiler. Normal `import` brings in code
that runs. Imports end in `.js` because that is the extension after compilation.

`Partial<Product>` makes every Product field optional. The lecture uses it for
incoming fields. `fields.price ?? 0` supplies zero when price is missing, while
preserving a supplied zero. `.map(...)` builds a new array by converting each
element of the old array.

## 6. Follow POST and DELETE

For POST, `express.json()` parses the JSON into `req.body`. Validation checks
that it is an object and that any supplied fields have the right types. Strings
stay strings; price and quantity must be nonnegative numbers, and quantity must
be a whole number. Omitted fields are allowed, matching the lecture's defaults.

`productService.add` selects the six known product fields and inserts them.
Mongo creates `_id`; the caller cannot choose it by supplying `id` or `_id`.
The controller responds with `201` and `{ "id": "..." }`. Use that id in GET.

For DELETE, Mongo's `deleteOne` returns a result with `deletedCount`. The service
turns that into a boolean. The controller sends `204` if one product was deleted,
or `404` if none was. Deleting twice should produce `204` then `404`.

Middleware order is important: parse JSON, route the request, handle unknown
routes, then handle errors. Error middleware has four parameters, with `err`
first; Express uses that signature to recognize it. Express 5 forwards rejected
Promises from async controllers to the error handler.

## 7. Run it yourself

From this repository folder:

```bash
npm ci
npm test
npm run typecheck
npm run build
```

Tests do not need your normal MongoDB server. To use Postman or curl, start your
normal MongoDB separately at `127.0.0.1:27017`. In Compass, connect to it, create
database `week4` and collection `products`, and import `sampleData/products.json`.
Then:

```bash
npm run dev
```

Keep that terminal open. In another terminal:

```bash
curl -i http://localhost:6790/products
curl -i -X POST http://localhost:6790/products \
  -H 'Content-Type: application/json' \
  -d '{"modelName":"Practice coffee maker","price":25,"quantity":3}'
```

Copy the new id from POST. Replace `YOUR_ID` below with it:

```bash
curl -i http://localhost:6790/products/YOUR_ID
curl -i -X DELETE http://localhost:6790/products/YOUR_ID
curl -i http://localhost:6790/products/YOUR_ID
```

Use a product you created for practice, because DELETE removes it. Expect `200`,
then `204` with an empty body, then `404`. Stop the dev server with Control-C.
`npm run build` followed by `npm start` runs compiled JavaScript instead of the
development watcher. Optional `MONGO_URI`, `DB_NAME`, and `PORT` overrides let you
point this app at a separate practice database.

If you get connection refused, MongoDB is not reachable at the chosen address.
If the port is in use, stop the other server or choose another `PORT`. If GET
returns `[]`, check the database/collection and your import. A 400 id error means
you did not replace `YOUR_ID` with a real 24-character id.

## 8. What the tests teach

Vitest runs the tests. Supertest sends HTTP requests to our Express app. A
MongoMemoryServer launches a real temporary MongoDB on a separate port. The
tests call `db.init(mongo.getUri(), 'week4-tests')` so they use that database.

`beforeAll` starts/connects once. `beforeEach` empties **only that test database's**
products and inserts the same two coffee makers. `afterEach` restores the one
service mock used for the error test. `afterAll` disconnects and stops MongoDB.
Never call `_clearCollection` on your normal database just to run these tests.

Each test follows arrange, act, assert. Setup arranges two known products; a
request acts; `expect` checks the result. GET's setup inserts directly through
the database layer so a broken POST does not cause a misleading GET failure.

Use `toBe` for values like a status number, `toEqual` for an object's contents,
`toHaveLength` for array size, and `toMatch` for an id's shape. Two separate
objects can have equal contents while failing `toBe`, which checks identity.

The suite checks list shape, creation/readback, successful and absent GET,
successful and absent DELETE, empty collections, defaults, invalid input,
unknown paths, and unexpected service errors. It runs the source tests once;
compiled copies under `dist/` are excluded.

Supertest imports `app.ts`, so it does not execute `index.ts`. Passing tests
alone do not prove the real server starts. That is why checking `npm start`
and actual HTTP requests is a separate step. GitHub's small classroom check
also does not substitute for these route checks or the instructor's review.

## 9. Practice and check yourself

1. Trace GET by id through the five layers without running it. At which point
   does the id become an ObjectId? At which point does it become a string again?
2. Create a product with price zero. Explain why `??` preserves that value.
3. Write a test that creates and deletes a product, then confirms GET returns 404.
4. Think through adding PATCH without coding it yet: which layer chooses the
   response status, which layer coordinates the update, and which talks to Mongo?
5. Change a test's expected status to the wrong number, run that test, and read
   the failure. Restore the expectation afterward.

Answers: ObjectId conversion happens in the database query; document-to-product
conversion makes the response id a string. `??` only replaces null/undefined,
not zero. The controller chooses HTTP status, the service coordinates the update,
and the database layer executes it. A good deletion test checks both the delete
response and a later read, rather than trusting the first response alone.

Suggested first session: spend ten minutes on requests/statuses, fifteen tracing
GET through the files, fifteen making POST/GET/DELETE requests, and ten reading
and changing one test. If you can explain why a missing product becomes null in
the service but 404 in the controller, you understand the central idea of today.
