Postman and Swagger
Before you start
You need: a running API — the one from Track 10.
Time: about 50 minutes, at the keyboard.
Learning objective
Build a Postman collection that exercises an API end to end, and use Swagger to explore an API you did not write.
Topics
- Requests and methods
- Headers, bodies and parameters
- Environments and variables
- Authentication and tokens
- Scripts and tests
- Collections and sharing
- Swagger UI
- Generating a collection from OpenAPI
- Diagnosing a failed request
Requests
GET https://localhost:7099/api/students?schoolId=1&page=1
POST https://localhost:7099/api/students
PUT https://localhost:7099/api/students/{publicId}
DELETE https://localhost:7099/api/students/{publicId}
Postman's tabs map to the parts of an HTTP request:
| Tab | Contains |
|---|---|
| Params | Query string — edits the URL and vice versa |
| Authorization | Auth scheme and credentials |
| Headers | Request headers |
| Body | The payload |
| Tests | JavaScript run after the response |
| Pre-request Script | JavaScript run before sending |
Postman is not a browser. It does not enforce CORS, does not send cookies automatically, and uses no browser cache. That is why "it works in Postman" is not evidence a browser call will work — the CORS article in the ASP.NET Core track explains why.
Headers and bodies
Content-Type: application/json
Accept: application/json
Authorization: Bearer eyJhbGciOi...
Body → raw → JSON sets Content-Type: application/json automatically.
{
"name": "Sneha Patel",
"rollNumber": "NCA-2024-0044",
"className": "9th",
"section": "A",
"dateOfBirth": "2010-03-14",
"parentName": "Mahesh Patel",
"parentPhone": "9812345670"
}
Choosing "raw → Text" instead of JSON sends text/plain, and an ASP.NET Core [FromBody] endpoint returns 415 Unsupported Media Type before your controller runs. That is one of the two most common Postman mistakes.
| Body type | Use for |
|---|---|
| raw → JSON | REST APIs |
| form-data | File uploads |
| x-www-form-urlencoded | HTML form posts |
| binary | A raw file body |
For a file upload, use form-data and do not set Content-Type manually — Postman sets multipart/form-data with the required boundary, and overriding it makes the body unparseable.
Environments and variables
{{baseUrl}}/api/students?schoolId={{schoolId}}
An environment is a named set of variables. Switching environments points the whole collection at a different server.
| Variable | Local | Staging |
|---|---|---|
baseUrl | https://localhost:7099 | https://api-staging.nexcoding.in |
schoolId | 1 | 7 |
token | (set by login) | (set by login) |
This is the feature that makes Postman worth using. Without it, testing against staging means editing every request.
| Scope | Lives in |
|---|---|
| Global | Everywhere |
| Environment | One environment — the usual choice |
| Collection | One collection |
| Local | One request run |
Mark secrets as secret type. Their values are masked in the UI and excluded from exports — which is what stops a token reaching a shared file.
Never commit a real token or password in an environment file. Export the environment with placeholder values and let each developer fill in their own.
Authentication and tokens
Authorization tab → Bearer Token → {{token}}.
Set it on the collection, not per request. Every request then inherits it, and there is one place to change.
Automating the login is the pattern that makes a collection usable:
// Login request → Tests tab
const response = pm.response.json();
pm.environment.set("token", response.accessToken);
pm.test("Login succeeded", () => {
pm.response.to.have.status(200);
pm.expect(response.accessToken).to.be.a("string");
});
Run the login once and every subsequent request carries a fresh token.
For a token that expires mid-run, refresh it automatically:
// Collection → Pre-request Script
const expiry = pm.environment.get("tokenExpiry");
if (!expiry || Date.now() > Number(expiry)) {
pm.sendRequest({
url: pm.environment.get("baseUrl") + "/api/auth/login",
method: "POST",
header: { "Content-Type": "application/json" },
body: {
mode: "raw",
raw: JSON.stringify({
email: pm.environment.get("email"),
password: pm.environment.get("password")
})
}
}, (error, response) => {
if (!error) {
const body = response.json();
pm.environment.set("token", body.accessToken);
pm.environment.set("tokenExpiry", Date.now() + 55 * 60 * 1000);
}
});
}
Postman also supports OAuth 2.0 and Basic auth directly in the Authorization tab, which avoids scripting for standard flows.
Scripts and tests
// Tests tab
pm.test("Status is 201", () => {
pm.response.to.have.status(201);
});
pm.test("Location header is present", () => {
pm.expect(pm.response.headers.get("Location")).to.include("/api/students/");
});
pm.test("Response has a publicId", () => {
const body = pm.response.json();
pm.expect(body.publicId).to.be.a("string");
pm.environment.set("createdStudentId", body.publicId);
});
pm.test("Responds within 500ms", () => {
pm.expect(pm.response.responseTime).to.be.below(500);
});
Storing the created id in a variable is what chains requests. Create → read → update → delete becomes a runnable sequence rather than four manual copy-pastes.
Failure paths deserve tests too:
pm.test("Rejects an invalid roll number", () => {
pm.response.to.have.status(400);
const body = pm.response.json();
pm.expect(body.errors).to.have.property("RollNumber");
});
Test what should fail, not only what should succeed. A collection asserting only happy paths does not notice when validation stops working.
// Pre-request Script
pm.environment.set("rollNumber", "NCA-2024-" + Math.floor(1000 + Math.random() * 9000));
Generating a unique value per run makes a create request re-runnable without a duplicate conflict.
Collections
NexCoding School Portal
├── Auth
│ └── Login
├── Students
│ ├── Search students
│ ├── Get student
│ ├── Create student
│ ├── Update student
│ └── Delete student
└── Fees
├── Outstanding report
└── Record payment
Collection Runner executes every request in order with its tests — a smoke test for the whole API in one click.
npm install -g newman
newman run school-portal.postman_collection.json \
-e local.postman_environment.json \
--reporters cli,junit
Newman runs a collection from the command line, which is what puts it in a CI pipeline. A pull request that breaks an endpoint then fails the build.
Commit the collection and a placeholder environment to the repository. It is executable documentation: a new developer imports it and has every endpoint working in a minute, with no guessing about headers or body shapes.
Export via Collection → … → Export → Collection v2.1.
Swagger UI
An ASP.NET Core API serves it at /swagger:
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
Swagger UI lists every endpoint with its parameters, request and response shapes, and status codes — generated from the code, so it cannot drift from the implementation.
"Try it out" sends a real request from the browser. That makes it the fastest way to explore an API you did not write, and the fastest way to check your own endpoint after a change.
The Authorize button
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "Paste the token only — Swagger adds the 'Bearer ' prefix."
});
Without a security definition there is no Authorize button, and every secured endpoint returns 401 in "Try it out" — reported as a broken API.
The description matters: pasting Bearer eyJ... where only the token is expected produces Bearer Bearer eyJ... and a 401 that looks like a bad token.
Documenting responses
/// <summary>Creates a student.</summary>
/// <response code="201">The student was created.</response>
/// <response code="400">Validation failed.</response>
/// <response code="409">The roll number is already in use.</response>
[HttpPost]
[ProducesResponseType(typeof(StudentDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<StudentDto>> Create(...) { }
ProducesResponseType is what makes the documentation accurate. Without it every endpoint is documented as returning 200 with an unknown body, which is worse than no documentation because it is confidently wrong.
XML comments need enabling in the project file:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
Swagger in production
Swagger UI in production is a published map of your API — every endpoint, every parameter, every model. That is reconnaissance for anyone probing it.
app.MapSwagger().RequireAuthorization("Monitoring");
Disable it, or require authorization.
Generating a collection from OpenAPI
/openapi.json (or /swagger/v1/swagger.json) is the machine-readable schema.
Postman → Import → paste the URL generates a collection with every endpoint, its parameters and example bodies. That is the fastest way to start testing an API you did not write.
The same file generates typed clients:
npx openapi-typescript https://localhost:7099/openapi.json -o src/api/schema.d.ts
A frontend generated from the schema cannot drift from the API — a renamed field becomes a TypeScript error rather than a runtime undefined.
Diagnosing a failed request
| Status | Meaning | Check |
|---|---|---|
| Could not send | Server unreachable | Is it running? Is the port right? |
| SSL error | Self-signed certificate | Settings → turn off SSL verification for localhost |
| 400 | Validation failed | Read the errors object — it names the fields |
| 401 | Not authenticated | Token missing, expired, or Bearer duplicated |
| 403 | Authenticated, not permitted | Wrong role — a different token will not help |
| 404 | Not found | Wrong URL, wrong verb, or the record does not exist |
| 405 | Method not allowed | Right URL, wrong verb |
| 415 | Unsupported media type | Body sent as Text, not JSON |
| 500 | Server failure | Check the API logs — the browser cannot tell you why |
401 versus 403 is the distinction that saves the most time: 401 means authenticate, 403 means authenticated and refused, so retrying with the same token is pointless.
Postman Console (Ctrl+Alt+C) shows the request exactly as sent — resolved variables, every header, the raw body. An unresolved {{token}} appearing literally in the header is visible there and nowhere else, and it is the most common cause of a mysterious 401.
Copy as cURL from browser DevTools, then Import → Raw text into Postman, reproduces a failing browser request exactly. Working in Postman but failing in the browser means CORS, a cookie, or a header the browser adds — a two-minute test that ends most frontend-versus-backend arguments.
A copied cURL contains your live token. Redact it before pasting it into a ticket.
Errors you will hit
| What you see | Cause | Fix |
|---|---|---|
| 415 Unsupported Media Type | Body sent as Text, not JSON | Body → raw → JSON |
| 401 with a token you just copied | Token expired, or {{token}} unresolved | Check the Postman Console |
{{baseUrl}} sent literally | No environment selected | Pick one, top right |
| Swagger's Authorize button is missing | AddSecurityDefinition not configured | Add it in the API |
| Works in Postman, fails in the browser | CORS — Postman does not enforce it | Fix it on the server |
| A request works alone but not in a run | Depends on an earlier request's variable | Order them, or set the variable |
The Postman Console shows the request actually sent, including unresolved {{variables}}. It answers most "but I set that" questions.
Common mistakes
- Body sent as Text rather than JSON, causing 415
- Setting
Content-Typemanually with form-data - Pasting
Bearer <token>into a Bearer Token field - Hardcoded URLs instead of
{{baseUrl}} - The token set per request instead of on the collection
- Real credentials committed in an exported environment
- No tests, so a collection proves nothing
- Testing only success paths
- Not chaining requests with variables
- Never opening the Postman Console
- Treating "works in Postman" as proof a browser call will work
- Swagger with no security definition
- No
ProducesResponseType, so the documentation is wrong - Swagger UI exposed in production
Practice
The course exercises are create a Postman collection and inspect a failed API call.
- Create a collection with folders for Auth, Students and Fees.
- Create Local and Staging environments with
baseUrlandschoolId. - Write a Login request that stores the token with
pm.environment.set. - Set Bearer auth on the collection using
{{token}}. Confirm every request inherits it. - Send a POST with the body as Text. Record the 415, then switch to JSON.
- Paste
Bearer eyJ...into the Bearer Token field. Record the 401 and find the doubled prefix in the Console. - Write tests for a create: 201, a
Locationheader, and apublicIdin the body. - Store the created id in a variable and chain a get, an update and a delete.
- Write a test asserting a 400 with a specific field in the
errorsobject. - Generate a random roll number in a pre-request script and run the create twice.
- Run the whole collection with the Collection Runner and read the summary.
- Run it with Newman and produce a JUnit report.
- Export the collection and a placeholder environment. Have someone else import and run them.
- Configure Swagger with a security definition. Authorize and call a secured endpoint.
- Remove the security definition and confirm the Authorize button disappears.
- Add
ProducesResponseTypeto an action and compare the documentation before and after. - Import
/openapi.jsoninto Postman and compare with your hand-built collection. - Copy a failing browser request as cURL, import it into Postman, and confirm it succeeds. Explain what that tells you.
Exercises 6 and 18 are the two that resolve the most support time.
You can now
- Build a collection that exercises an API end to end
- Use environments and variables
- Capture a token automatically in a test script
- Read Swagger and authorise a request from it
- Say why Postman working proves nothing about CORS
Review questions
- Why does sending a body as Text produce a 415?
- What does the Postman Console show that the request tabs do not?
- Why is "it works in Postman" not proof a browser call will work?
- What does
ProducesResponseTypechange about generated documentation?