swadhinbiswas

[email protected]
2026-07-25 21:46:34 +0600 committed NaN years ago
9c4927f
commit 9c4927f
7 files changed
+760 -131
▼docs_site/src/content/docs/advanced/custom-response.md
+89-5
1
1
---
2
2
title: Custom Response Classes
3
3
description: Use HTML, streaming, file, and other custom response types in JustAPI.
4
-keywords: [JustAPI, custom response, HTML, streaming, file response, redirect]
4
+keywords: [JustAPI, custom response, HTML, streaming, file response, redirect, response_class]
5
5
---
6
6
7
-## HTML Response
7
+## Using response_class in Decorator
8
+
9
+Set the response class directly in the decorator:
8
10
9
11
```python
10
-from starlette.responses import HTMLResponse
12
+from starlette.responses import HTMLResponse, PlainTextResponse
11
13
from justapi import JustAPIApp
12
14
13
15
app = JustAPIApp()
14
16
17
+@app.get("/", response_class=PlainTextResponse)
18
+def root():
19
+ return "Hello, plain text!"
20
+```
21
+
22
+This also documents the correct Content-Type in the OpenAPI schema.
23
+
24
+## HTML Response
25
+
26
+```python
15
27
@app.get("/html")
16
28
def html_page():
17
29
return HTMLResponse(content="<h1>Hello</h1><p>This is HTML</p>")
18
30
```
19
31
32
+Or using `response_class`:
33
+
34
+```python
35
+@app.get("/html", response_class=HTMLResponse)
36
+def html_page():
37
+ return "<h1>Hello</h1>"
38
+```
39
+
20
40
## Streaming Response
21
41
22
42
```python
33
53
return StreamingResponse(generate_tokens(), media_type="text/event-stream")
34
54
```
35
55
56
+## Redirect
57
+
58
+```python
59
+from starlette.responses import RedirectResponse
60
+
61
+@app.get("/old-path")
62
+def old_path():
63
+ return RedirectResponse(url="/new-path", status_code=302)
64
+
65
+@app.get("/new-path")
66
+def new_path():
67
+ return {"message": "you are here"}
68
+```
69
+
70
+Or using `response_class`:
71
+
72
+```python
73
+@app.get("/old-path", response_class=RedirectResponse, status_code=302)
74
+def old_path():
75
+ return "/new-path"
76
+```
77
+
36
78
## File Response
37
79
38
80
```python
43
85
return FileResponse("report.pdf", filename="report.pdf")
44
86
```
45
87
46
-## Plain Text Response
88
+## JSON Response with Custom Headers
47
89
48
90
```python
49
-from starlette.responses import PlainTextResponse
91
+from starlette.responses import JSONResponse
92
+
93
+@app.get("/custom")
94
+def custom():
95
+ return JSONResponse(
96
+ content={"data": "value"},
97
+ headers={"X-Custom": "header-value"},
98
+ )
99
+```
100
+
101
+## Plain Text Response
50
102
103
+```python
51
104
@app.get("/text")
52
105
def text():
53
106
return PlainTextResponse("Hello, plain text!")
67
120
`ORJSONResponse` is faster than `JSONResponse` for large payloads. Install with `pip install orjson`.
68
121
:::
69
122
123
+## Custom Response Class
124
+
125
+Create your own response class:
126
+
127
+```python
128
+from starlette.responses import JSONResponse
129
+
130
+class PrettyJSONResponse(JSONResponse):
131
+ def render(self, content) -> bytes:
132
+ import json
133
+ return json.dumps(
134
+ content,
135
+ indent=2,
136
+ ensure_ascii=False,
137
+ ).encode("utf-8")
138
+
139
+@app.get("/pretty")
140
+def pretty():
141
+ return PrettyJSONResponse(content={"data": "value"})
142
+```
143
+
144
+## Response Class + Status Code
145
+
146
+Combine `response_class` with `status_code`:
147
+
148
+```python
149
+@app.post("/items", response_class=JSONResponse, status_code=201)
150
+def create_item():
151
+ return {"message": "created"}
152
+```
153
+
70
154
## See Also
71
155
72
156
- [Return a Response Directly](/advanced/response-directly/) — basic response object usage
73
157
▼docs_site/src/content/docs/tutorials/body-nested-models.md
+103-2
1
1
---
2
2
title: Body — Nested Models
3
-description: Use nested Pydantic models for complex JSON structures in JustAPI.
4
-keywords: [JustAPI, nested models, Pydantic, complex JSON, deeply nested]
3
+description: Use nested Pydantic models for complex JSON structures in JustAPI — sets, lists, dicts, special types.
4
+keywords: [JustAPI, nested models, Pydantic, complex JSON, sets, lists, dicts, HttpUrl]
5
5
---
6
6
7
7
## Basic Nested Models
71
71
}
72
72
```
73
73
74
+## Set Types
75
+
76
+Use `set` to deduplicate values:
77
+
78
+```python
79
+class Item(BaseModel):
80
+ name: str
81
+ tags: set[str] = set()
82
+
83
+@app.post("/items")
84
+def create_item(item: Item):
85
+ return item.model_dump()
86
+```
87
+
88
+```json
89
+{
90
+ "name": "Widget",
91
+ "tags": ["hardware", "sale", "hardware"]
92
+}
93
+```
94
+
95
+The response deduplicates:
96
+
97
+```json
98
+{
99
+ "name": "Widget",
100
+ "tags": ["hardware", "sale"]
101
+}
102
+```
103
+
104
+## Bodies of Pure Lists
105
+
106
+The entire request body can be a list:
107
+
108
+```python
109
+class Image(BaseModel):
110
+ url: str
111
+ caption: str = ""
112
+
113
+@app.post("/images")
114
+def create_images(images: list[Image]):
115
+ return {"count": len(images), "images": images}
116
+```
117
+
118
+```json
119
+[
120
+ {"url": "https://example.com/img1.jpg", "caption": "Photo 1"},
121
+ {"url": "https://example.com/img2.jpg"}
122
+]
123
+```
124
+
125
+## Bodies of Arbitrary Dicts
126
+
127
+Use `dict` for dynamic keys:
128
+
129
+```python
130
+@app.post("/metrics")
131
+def record_metrics(metrics: dict[str, float]):
132
+ return {"metrics": metrics}
133
+```
134
+
135
+```json
136
+{
137
+ "cpu": 75.5,
138
+ "memory": 62.3,
139
+ "disk": 45.1
140
+}
141
+```
142
+
143
+Typed keys:
144
+
145
+```python
146
+@app.post("/config")
147
+def update_config(config: dict[str, str]):
148
+ return {"config": config}
149
+```
150
+
151
+## Special Types
152
+
153
+Pydantic includes types for common formats:
154
+
155
+```python
156
+from pydantic import BaseModel, HttpUrl, EmailStr
157
+
158
+class Website(BaseModel):
159
+ url: HttpUrl
160
+ contact_email: EmailStr
161
+
162
+@app.post("/websites")
163
+def create_website(website: Website):
164
+ return {"url": str(website.url), "email": str(website.contact_email)}
165
+```
166
+
167
+| Type | Validates |
168
+|------|-----------|
169
+| `HttpUrl` | Valid HTTP/HTTPS URL |
170
+| `EmailStr` | Valid email address |
171
+| `AnyUrl` | Any valid URL |
172
+| `IPvAnyAddress` | Valid IP address |
173
+| `AnyHttpUrl` | HTTP or HTTPS URL |
174
+
74
175
## Deeply Nested Structures
75
176
76
177
```python
77
178
▼docs_site/src/content/docs/tutorials/error-handling.md
+108-101
1
1
---
2
-title: Error Handling
3
-description: Handle errors gracefully in JustAPI with HTTPException, custom exception handlers, and validation error responses — a FastAPI alternative.
4
-keywords: error handling, HTTPException, exception handlers, JustAPI, FastAPI alternative, validation errors
2
+title: Handling Errors
3
+description: Handle HTTP errors, validation errors, and custom exception handlers in JustAPI.
4
+keywords: [JustAPI, error handling, HTTPException, RequestValidationError, exception handlers]
5
5
---
6
6
7
-JustAPI provides a structured error-handling system that lets you return consistent error responses across your API.
7
+## HTTPException
8
8
9
-## Raising HTTP Errors
10
-
11
-Use `HTTPException` to return error responses from any handler or dependency:
9
+Raise an `HTTPException` to return an error response:
12
10
13
11
```python
14
12
from justapi import JustAPIApp, HTTPException
15
13
16
14
app = JustAPIApp()
17
15
16
+@app.get("/items/{item_id}")
17
+def get_item(item_id: int):
18
+ if item_id == 0:
19
+ raise HTTPException(status_code=404, detail="Item not found")
20
+ return {"item_id": item_id}
21
+```
22
+
23
+### Adding Headers
18
24
25
+```python
19
26
@app.get("/items/{item_id}")
20
-def read_item(request, item_id: int):
27
+def get_item(item_id: int):
21
28
if item_id == 0:
22
- raise HTTPException(status_code=400, detail="Item ID must be positive")
23
- if item_id == 42:
24
29
raise HTTPException(
25
- status_code=418,
26
- detail="I'm a teapot",
27
- headers={"X-Error": "teapot"},
30
+ status_code=404,
31
+ detail="Item not found",
32
+ headers={"X-Error": "item-not-found"},
28
33
)
29
34
return {"item_id": item_id}
30
35
```
31
36
32
-```bash
33
-curl http://127.0.0.1:8000/items/0
34
-# Output: {"detail":"Item ID must be positive"}
37
+## RequestValidationError
35
38
36
-curl http://127.0.0.1:8000/items/42
37
-# Output: {"detail":"I'm a teapot"}
38
-```
39
+When request data doesn't match the schema, JustAPI returns a `422` error automatically:
39
40
40
-### `HTTPException` Parameters
41
+```json
42
+{
43
+ "detail": [
44
+ {
45
+ "type": "int_parsing",
46
+ "loc": ["path", "item_id"],
47
+ "msg": "Input should be a valid integer",
48
+ "input": "foo"
49
+ }
50
+ ]
51
+}
52
+```
41
53
42
-| Parameter | Type | Default | Description |
43
-|---|---|---|---|
44
-| `status_code` | int | required | HTTP status code |
45
-| `detail` | str | required | Error message |
46
-| `headers` | dict | `None` | Additional response headers |
54
+The `detail` array contains every validation error with:
55
+- `type` — error category
56
+- `loc` — where the error occurred (`["path", "body", "query", "header"]`)
57
+- `msg` — human-readable message
58
+- `input` — the value that was rejected
47
59
48
-## Custom Exception Handlers
60
+## Override Validation Error Handler
49
61
50
-Override the default error format for specific exception types:
62
+Customize the validation error response:
51
63
52
64
```python
53
-from justapi import RequestValidationError
54
-
55
-
56
-@app.exception_handler(HTTPException)
57
-def http_exception_handler(request, exc: HTTPException):
58
- return {
59
- "status": exc.status_code,
60
- "body": {
61
- "error": "Request Failed",
62
- "code": exc.status_code,
63
- "message": exc.detail,
64
- },
65
- }
65
+from justapi import JustAPIApp
66
+from starlette.requests import Request
67
+from starlette.responses import JSONResponse
66
68
69
+app = JustAPIApp()
67
70
68
71
@app.exception_handler(RequestValidationError)
69
-def validation_handler(request, exc: RequestValidationError):
70
- return {
71
- "status": 422,
72
- "body": {
73
- "error": "Validation Failed",
74
- "issues": exc.errors(),
72
+async def validation_error_handler(request: Request, exc):
73
+ return JSONResponse(
74
+ status_code=422,
75
+ content={
76
+ "error": "validation_error",
77
+ "message": "Invalid request data",
78
+ "details": exc.errors(),
75
79
},
76
- }
80
+ )
77
81
```
78
82
79
-## Validation Errors
83
+## Override HTTPException Handler
80
84
81
-When a request fails type validation (e.g., invalid path parameter type), JustAPI returns a structured 422 response:
85
+Change the default HTTP error format:
82
86
83
-```bash
84
-curl http://127.0.0.1:8000/items/abc
85
-# Output: {"detail":"validation error"}
86
-```
87
-
88
-With a custom handler, you can expand this to include field-level details.
89
-
90
-## Error Response Format
91
-
92
-By default, all error responses follow the format:
87
+```python
88
+from fastapi.exception_handlers import http_exception_handler
93
89
94
-```json
95
-{
96
- "detail": "<error message>"
97
-}
90
+@app.exception_handler(HTTPException)
91
+async def custom_http_exception(request, exc):
92
+ return JSONResponse(
93
+ status_code=exc.status_code,
94
+ content={
95
+ "error": "http_error",
96
+ "message": exc.detail,
97
+ "status_code": exc.status_code,
98
+ },
99
+ )
98
100
```
99
101
100
-This applies to:
101
-- **400** — Bad request
102
-- **401** — Unauthorized
103
-- **403** — Forbidden
104
-- **404** — Not found (unmatched route)
105
-- **413** — Payload too large
106
-- **422** — Validation error
107
-- **429** — Rate limit exceeded
108
-- **500** — Internal server error
109
-- **503** — Service unavailable (pool saturation)
110
-- **504** — Gateway timeout
111
-
112
-## Handling Errors in Dependencies
102
+## Custom Exception
113
103
114
-Dependencies can raise `HTTPException` to reject requests:
104
+Create your own exception and handler:
115
105
116
106
```python
117
-from justapi import Depends, HTTPException, Header
118
-
119
-
120
-def require_admin(authorization: str = Header(...)):
121
- if authorization != "Bearer admin-token":
122
- raise HTTPException(status_code=403, detail="Admin access required")
107
+class InsufficientFundsError(Exception):
108
+ def __init__(self, balance: float, amount: float):
109
+ self.balance = balance
110
+ self.amount = amount
111
+
112
+@app.exception_handler(InsufficientFundsError)
113
+async def insufficient_funds_handler(request, exc):
114
+ return JSONResponse(
115
+ status_code=402,
116
+ content={
117
+ "error": "insufficient_funds",
118
+ "message": f"Need {exc.amount}, have {exc.balance}",
119
+ },
120
+ )
121
+
122
+@app.post("/purchase")
123
+def purchase(amount: float):
124
+ balance = 100.0
125
+ if amount > balance:
126
+ raise InsufficientFundsError(balance, amount)
127
+ return {"status": "ok"}
128
+```
123
129
130
+## Debug Mode
124
131
125
-@app.get("/admin")
126
-def admin_panel(request, admin: None = Depends(require_admin)):
127
- return {"secret": "classified"}
132
+```python
133
+app = JustAPIApp(debug=True)
128
134
```
129
135
130
-## Global Error Catchers
136
+Debug mode returns detailed tracebacks in responses.
131
137
132
-You can register a catch-all exception handler:
138
+:::warning
139
+Never use `debug=True` in production. It exposes internal details.
140
+:::
133
141
134
-```python
135
-@app.exception_handler(Exception)
136
-def global_handler(request, exc: Exception):
137
- return {
138
- "status": 500,
139
- "body": {"detail": "An unexpected error occurred"},
140
- }
141
-```
142
+## Common Errors
142
143
143
-In production, JustAPI's `panic = "abort"` mode ensures that unexpected Rust panics abort the process cleanly, and the supervisor restarts it. See [Production Checklist](/deployment/production-checklist/).
144
+| Status Code | Meaning | Cause |
145
+|-------------|---------|-------|
146
+| 404 | Not Found | Route doesn't exist |
147
+| 405 | Method Not Allowed | Wrong HTTP method |
148
+| 422 | Validation Error | Request data doesn't match schema |
149
+| 401 | Unauthorized | Missing or invalid auth |
150
+| 403 | Forbidden | Authenticated but not authorized |
144
151
145
-## Next Steps
152
+## See Also
146
153
147
-- [Middleware](/tutorials/middleware/) — Request/response interception
148
-- [File Uploads](/tutorials/file-uploads/) — Handle multipart form data
149
-- [Production Checklist](/deployment/production-checklist/) — Error handling in production
154
+- [Status Codes](/tutorials/response-status-code/) — custom status codes
155
+- [Error Codes Reference](/reference/error-codes/) — complete error code list
156
+- [Advanced Security](/advanced/advanced-security/) — auth errors
150
157
▼docs_site/src/content/docs/tutorials/query-params-str-validations.md
+114-5
1
1
---
2
2
title: Query Parameters and String Validations
3
-description: Add string validation constraints to query parameters — min_length, max_length, regex patterns.
3
+description: Add string validation constraints to query parameters — min_length, max_length, regex patterns, metadata.
4
4
keywords: [JustAPI, query parameters, validation, min_length, max_length, regex, Query]
5
5
---
6
6
21
21
return {"q": q, "tag": tag}
22
22
```
23
23
24
+`Query(...)` means the parameter is required (the `...` is Python's Ellipsis, meaning "no default"). `Query(None)` makes it optional.
25
+
26
+## Required vs Optional
27
+
28
+```python
29
+# Required — no default value
30
+@app.get("/search")
31
+def search(q: str = Query(...)):
32
+ return {"q": q}
33
+
34
+# Optional — default value provided
35
+@app.get("/search")
36
+def search(q: str = Query(None)):
37
+ return {"q": q}
38
+
39
+# Optional with explicit default
40
+@app.get("/search")
41
+def search(q: str = Query("default")):
42
+ return {"q": q}
43
+```
44
+
45
+:::note
46
+`Query(...)` and `Query(min_length=1)` are both required. The `...` just means "no default value" — the constraint is separate.
47
+:::
48
+
24
49
## Validation Options
25
50
26
51
| Parameter | Type | Description |
32
57
| `description` | `str` | Description shown in the docs |
33
58
| `deprecated` | `bool` | Mark as deprecated in the docs |
34
59
| `alias` | `str` | Alternative parameter name |
60
+| `include_in_schema` | `bool` | Set `False` to hide from docs |
35
61
36
62
## Regex Patterns
37
63
44
70
return {"email": email, "phone": phone}
45
71
```
46
72
73
+If the pattern doesn't match, the API returns a validation error:
74
+
75
+```json
76
+{
77
+ "detail": [
78
+ {
79
+ "type": "string_pattern_mismatch",
80
+ "loc": ["query", "email"],
81
+ "msg": "String should match pattern '^[\\w.-]+@[\\w.-]+\\.\\w+$'",
82
+ "input": "not-an-email"
83
+ }
84
+ ]
85
+}
86
+```
87
+
47
88
:::tip
48
-Use raw strings (`r"..."`) for regex patterns to avoid escaping issues.
89
+Use raw strings (`r"..."`) for regex patterns to avoid escaping issues with backslashes.
49
90
:::
50
91
92
+## Multiple Values for One Parameter
93
+
94
+Repeat the parameter in the query string:
95
+
96
+```python
97
+@app.get("/filter")
98
+def filter_items(tags: list[str] = Query(None)):
99
+ return {"tags": tags}
100
+```
101
+
102
+`/filter?tags=a&tags=b&tags=c` returns `{"tags": ["a", "b", "c"]}`.
103
+
104
+With validation:
105
+
106
+```python
107
+@app.get("/filter")
108
+def filter_items(
109
+ tags: list[str] = Query(None, min_length=1, max_length=10),
110
+):
111
+ return {"tags": tags}
112
+```
113
+
114
+Each tag must be between 1 and 10 characters.
115
+
116
+## Metadata in Docs
117
+
118
+Add title and description to show in the OpenAPI schema:
119
+
120
+```python
121
+@app.get("/search")
122
+def search(
123
+ q: str = Query(
124
+ ...,
125
+ title="Search Query",
126
+ description="The search term to look for",
127
+ min_length=1,
128
+ max_length=200,
129
+ ),
130
+):
131
+ return {"q": q}
132
+```
133
+
134
+The Swagger UI will show the title and description next to the parameter.
135
+
51
136
## Aliasing
52
137
53
138
Use `alias` when the parameter name differs from the Python variable name:
64
149
65
150
```python
66
151
@app.get("/search")
67
-def search(q: str = Query(..., deprecated=True)):
68
- """Use the new search endpoint instead."""
152
+def search(
153
+ q: str = Query(...),
154
+ old_format: bool = Query(False, deprecated=True),
155
+):
156
+ return {"q": q, "old_format": old_format}
157
+```
158
+
159
+The `old_format` parameter appears in the docs with a deprecation notice.
160
+
161
+## Hiding Parameters from Docs
162
+
163
+```python
164
+@app.get("/search")
165
+def search(
166
+ q: str = Query(...),
167
+ internal_flag: str = Query(None, include_in_schema=False),
168
+):
69
169
return {"q": q}
70
170
```
71
171
72
-The parameter appears in the docs with a deprecation notice.
172
+`internal_flag` works but doesn't appear in the OpenAPI schema.
173
+
174
+## Recap
175
+
176
+- `Query(...)` makes a parameter required, `Query(None)` makes it optional
177
+- Use `min_length`, `max_length`, `pattern` for string validation
178
+- Use `title` and `description` for better docs
179
+- Use `alias` for alternative parameter names
180
+- Use `deprecated=True` to mark parameters as deprecated
181
+- Use `list[str]` for repeating query parameters
73
182
74
183
## See Also
75
184
76
185
▼docs_site/src/content/docs/tutorials/response-model.md
+85-2
1
1
---
2
2
title: Response Model — Return Type
3
3
description: Control the response schema and filter output fields in JustAPI with response_model.
4
-keywords: [JustAPI, response model, return type, filtering, schema]
4
+keywords: [JustAPI, response model, return type, filtering, schema, exclude_unset]
5
5
---
6
6
7
7
## Basic Response Model
25
25
return {"id": user_id, "name": "Alice", "email": "[email protected]", "password": "secret"}
26
26
```
27
27
28
-The `password` field is automatically excluded from the response because it's not declared in the response model.
28
+The `password` field is automatically excluded from the response because it's not in the response model. This is a security feature — you don't accidentally leak sensitive data.
29
29
30
30
## Filtering Response Fields
31
31
32
+Use a separate model for output:
33
+
32
34
```python
33
35
class UserOut(BaseModel):
34
36
id: int
53
55
return {"id": user_id, "name": "Alice", "email": "[email protected]", "password": "secret"}
54
56
```
55
57
58
+## Include Fields
59
+
60
+```python
61
+@app.get(
62
+ "/users/{user_id}",
63
+ response_model=UserOut,
64
+ response_model_include={"id", "name"},
65
+)
66
+def get_user(user_id: int):
67
+ return {"id": user_id, "name": "Alice"}
68
+```
69
+
70
+## response_model_exclude_unset
71
+
72
+This is useful when working with databases that return optional fields. It excludes fields that were not explicitly set:
73
+
74
+```python
75
+from pydantic import BaseModel
76
+from typing import Optional
77
+
78
+class User(BaseModel):
79
+ id: int
80
+ name: str
81
+ email: Optional[str] = None
82
+ bio: Optional[str] = None
83
+
84
+app = JustAPIApp()
85
+
86
+@app.get("/users/{user_id}", response_model=User, response_model_exclude_unset=True)
87
+def get_user(user_id: int):
88
+ # Database returns only set fields
89
+ return {"id": 1, "name": "Alice"}
90
+ # email and bio are excluded because they weren't set
91
+```
92
+
93
+Without `exclude_unset`, the response would include `"email": null, "bio": null`. With it, only `id` and `name` appear.
94
+
95
+## response_model_exclude_defaults
96
+
97
+Excludes fields that have their default value:
98
+
99
+```python
100
+@app.get("/users/{user_id}", response_model=User, response_model_exclude_defaults=True)
101
+def get_user(user_id: int):
102
+ return {"id": 1, "name": "Alice", "email": None, "bio": None}
103
+ # email and bio are excluded because None is the default
104
+```
105
+
106
+## response_model_exclude_none
107
+
108
+Excludes fields that are `None`:
109
+
110
+```python
111
+@app.get("/users/{user_id}", response_model=User, response_model_exclude_none=True)
112
+def get_user(user_id: int):
113
+ return {"id": 1, "name": "Alice", "email": None, "bio": None}
114
+ # email and bio are excluded because they're None
115
+```
116
+
117
+## Disable Response Model
118
+
119
+```python
120
+@app.get("/raw", response_model=None)
121
+def raw():
122
+ return {"anything": "goes"}
123
+```
124
+
125
+With `response_model=None`, no response filtering is applied.
126
+
56
127
## Multiple Response Models
57
128
58
129
```python
69
140
return {"id": user_id, "name": "Alice"}
70
141
```
71
142
143
+## Return Type Annotation
144
+
145
+You can also use the Python return type annotation:
146
+
147
+```python
148
+@app.get("/users/{user_id}")
149
+def get_user(user_id: int) -> UserOut:
150
+ return {"id": user_id, "name": "Alice"}
151
+```
152
+
153
+JustAPI uses the return type as the response model automatically.
154
+
72
155
## See Also
73
156
74
157
- [Extra Models](/tutorials/extra-models/) — multiple related models
75
158
▼docs_site/src/content/docs/tutorials/security/first-steps/index.md
+156-10
1
1
---
2
2
title: Security — First Steps
3
-description: Add basic authentication to a JustAPI route.
4
-keywords: [JustAPI, security, authentication, OAuth2, bearer token]
3
+description: Add authentication to a JustAPI route using OAuth2 and Security dependencies.
4
+keywords: [JustAPI, security, OAuth2, authentication, bearer token, password flow]
5
5
---
6
6
7
-## Adding Security to a Route
7
+This tutorial builds a simple authentication system step by step. By the end you'll understand the full OAuth2 password flow and how to protect routes.
8
8
9
-Use `Security()` (like `Depends()` but for security) to protect routes:
9
+## The Problem
10
+
11
+You need to protect a route so only authenticated users can access it. Unauthenticated requests should get a `401` error.
12
+
13
+## Step 1: Add a Security Dependency
14
+
15
+```python
16
+from justapi import JustAPIApp, Security
17
+from fastapi.security import OAuth2PasswordBearer
18
+
19
+app = JustAPIApp()
20
+
21
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
22
+
23
+@app.get("/users/me")
24
+def read_users_me(token: str = Security(oauth2_scheme)):
25
+ return {"token": token}
26
+```
27
+
28
+### What happens here?
29
+
30
+1. `OAuth2PasswordBearer(tokenUrl="token")` creates a dependency that expects an `Authorization: Bearer <token>` header.
31
+2. `Security(oauth2_scheme)` tells JustAPI this is a security requirement — it appears in the OpenAPI docs with an "Authorize" button.
32
+3. The `token` parameter receives the raw token string from the header.
33
+
34
+If the request has no `Authorization` header, JustAPI returns a `401` error automatically — no code needed.
35
+
36
+### Try it
37
+
38
+Start the server and go to `http://localhost:8000/docs`. You'll see an **Authorize** button at the top.
39
+
40
+Click it, enter any value, and then call `/users/me`. The request includes the header automatically.
41
+
42
+Without the header:
43
+
44
+```json
45
+{
46
+ "detail": "Not authenticated"
47
+}
48
+```
49
+
50
+## Step 2: Validate the Token
51
+
52
+The token is just a string. We need to verify it:
10
53
11
54
```python
12
-from justapi import JustAPIApp, Security, Depends
55
+from justapi import JustAPIApp, Security, HTTPException
13
56
from fastapi.security import OAuth2PasswordBearer
14
57
15
58
app = JustAPIApp()
59
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
60
+
61
+fake_users_db = {
62
+ "alice": {"user_id": 1, "name": "Alice"},
63
+ "bob": {"user_id": 2, "name": "Bob"},
64
+}
65
+
66
+def get_current_user(token: str = Security(oauth2_scheme)):
67
+ if token not in fake_users_db:
68
+ raise HTTPException(status_code=401, detail="Invalid token")
69
+ return fake_users_db[token]
70
+
71
+@app.get("/users/me")
72
+def read_users_me(current_user: dict = Security(get_current_user)):
73
+ return current_user
74
+```
75
+
76
+Now the flow is:
77
+
78
+1. Client sends `Authorization: Bearer alice`
79
+2. `oauth2_scheme` extracts `"alice"` as the token
80
+3. `get_current_user` looks up `"alice"` in the database
81
+4. If found, returns the user dict — which becomes `current_user` in the handler
82
+5. If not found, raises `401`
16
83
84
+:::note
85
+The token `"alice"` is the actual value after `Bearer` in the header. In production this would be a JWT or opaque token.
86
+:::
87
+
88
+## Step 3: Add a Token Endpoint
89
+
90
+The frontend needs a way to log in and get a token. Create a `/token` endpoint:
91
+
92
+```python
93
+from justapi import JustAPIApp, Security, HTTPException
94
+from fastapi.security import OAuth2PasswordBearer
95
+from pydantic import BaseModel
96
+
97
+app = JustAPIApp()
17
98
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
18
99
100
+class TokenRequest(BaseModel):
101
+ username: str
102
+ password: str
103
+
104
+fake_users_db = {
105
+ "alice": {"user_id": 1, "name": "Alice", "password": "secret123"},
106
+ "bob": {"user_id": 2, "name": "Bob", "password": "pass456"},
107
+}
108
+
19
109
def get_current_user(token: str = Security(oauth2_scheme)):
20
- # In production, decode the token
21
- return {"user_id": 1, "name": "Alice"}
110
+ if token not in fake_users_db:
111
+ raise HTTPException(status_code=401, detail="Invalid token")
112
+ return fake_users_db[token]
113
+
114
+@app.post("/token")
115
+def login(request: TokenRequest):
116
+ user = fake_users_db.get(request.username)
117
+ if not user or user["password"] != request.password:
118
+ raise HTTPException(status_code=401, detail="Wrong username or password")
119
+ return {"access_token": request.username, "token_type": "bearer"}
22
120
23
121
@app.get("/users/me")
24
122
def read_users_me(current_user: dict = Security(get_current_user)):
25
123
return current_user
26
124
```
27
125
28
-## OAuth2PasswordBearer
126
+### The full flow
127
+
128
+1. Client sends `POST /token` with `{"username": "alice", "password": "secret123"}`
129
+2. Server returns `{"access_token": "alice", "token_type": "bearer"}`
130
+3. Client sends `GET /users/me` with header `Authorization: Bearer alice`
131
+4. Server returns `{"user_id": 1, "name": "Alice"}`
132
+
133
+:::tip
134
+In the Swagger UI (`/docs`), you can now click **Authorize**, enter `alice` / `secret123`, and all subsequent requests will include the token automatically.
135
+:::
136
+
137
+## Step 4: Add User Roles
138
+
139
+Extend the dependency to check permissions:
140
+
141
+```python
142
+def get_current_user(token: str = Security(oauth2_scheme)):
143
+ if token not in fake_users_db:
144
+ raise HTTPException(status_code=401, detail="Invalid token")
145
+ return fake_users_db[token]
146
+
147
+def require_admin(current_user: dict = Security(get_current_user)):
148
+ if current_user.get("role") != "admin":
149
+ raise HTTPException(status_code=403, detail="Admin access required")
150
+ return current_user
151
+
152
+@app.get("/admin/dashboard")
153
+def admin_dashboard(admin: dict = Security(require_admin)):
154
+ return {"message": f"Welcome admin {admin['name']}"}
155
+```
156
+
157
+Now `/admin/dashboard` requires both a valid token AND admin role.
158
+
159
+## What JustAPI Does Under the Hood
160
+
161
+When you use `Security(oauth2_scheme)`:
162
+
163
+1. **Before the request**: JustAPI checks for the `Authorization` header
164
+2. **Missing header**: Returns `401` with `{"detail": "Not authenticated"}`
165
+3. **Invalid header format**: Returns `401` with details about the expected format
166
+4. **Valid header**: Extracts the token and passes it to your dependency
167
+5. **In the docs**: Adds an "Authorize" button to Swagger UI
168
+
169
+## Recap
29
170
30
-The `tokenUrl` tells the docs where to send login requests. It appears as an "Authorize" button in Swagger UI.
171
+- `OAuth2PasswordBearer(tokenUrl="token")` creates the auth dependency
172
+- `Security()` marks it as a security requirement in the docs
173
+- Your dependency validates the token and returns the user
174
+- Unauthenticated requests get a `401` automatically
175
+- Chain dependencies for role-based access control
31
176
32
177
## See Also
33
178
34
179
- [Get Current User](/tutorials/security/get-current-user/) — extracting user from token
35
180
- [Simple OAuth2](/tutorials/security/simple-oauth2/) — password-based OAuth2
36
-- [OAuth2 + JWT](/tutorials/security/oauth2-jwt/) — full JWT implementation
181
+- [OAuth2 + JWT Tokens](/tutorials/security/oauth2-jwt/) — production JWT implementation
182
+- [Advanced Security](/advanced/advanced-security/) — OAuth2 scopes, API keys
37
183
▼docs_site/src/content/docs/tutorials/testing.md
+105-6
1
1
---
2
2
title: Testing
3
3
description: Test JustAPI applications using the built-in JustAPITestClient for unit and integration tests.
4
-keywords: [JustAPI, testing, test client, pytest, assertions]
4
+keywords: [JustAPI, testing, test client, pytest, assertions, CRUD]
5
5
---
6
6
7
7
## JustAPITestClient
21
21
22
22
def test_hello():
23
23
response = client.get("/hello")
24
- assert response.status == 200
24
+ assert response.status_code == 200
25
25
assert response.json() == {"message": "Hello!"}
26
26
```
27
27
30
30
```python
31
31
def test_create_item():
32
32
response = client.post("/items", body={"name": "Widget", "price": 9.99})
33
- assert response.status == 200
33
+ assert response.status_code == 200
34
34
assert response.json()["name"] == "Widget"
35
35
```
36
36
37
+## Testing with Headers
38
+
39
+```python
40
+@app.get("/protected")
41
+def protected(x_token: str = Header(...)):
42
+ return {"token": x_token}
43
+
44
+def test_with_header():
45
+ response = client.get("/protected", headers={"X-Token": "secret"})
46
+ assert response.status_code == 200
47
+ assert response.json() == {"token": "secret"}
48
+```
49
+
37
50
## Testing Error Responses
38
51
39
52
```python
40
53
def test_not_found():
41
54
response = client.get("/nonexistent")
42
- assert response.status == 404
55
+ assert response.status_code == 404
43
56
44
57
def test_validation_error():
45
58
response = client.post("/items", body={"name": "x"})
46
- assert response.status == 422 # validation error
59
+ assert response.status_code == 422
60
+ assert "detail" in response.json()
47
61
```
48
62
49
63
## Testing with Path Parameters
74
88
assert response.json() == {"q": "foo", "limit": 5}
75
89
```
76
90
91
+## Full CRUD Example
92
+
93
+Here's a complete example with a mini CRUD API and tests:
94
+
95
+```python
96
+from justapi import JustAPIApp, JustAPITestClient, HTTPException
97
+
98
+app = JustAPIApp()
99
+items_db = {}
100
+
101
+@app.post("/items", status_code=201)
102
+def create_item(item: dict):
103
+ item_id = len(items_db) + 1
104
+ items_db[item_id] = item
105
+ return {"id": item_id, **item}
106
+
107
+@app.get("/items/{item_id}")
108
+def get_item(item_id: int):
109
+ if item_id not in items_db:
110
+ raise HTTPException(status_code=404, detail="Not found")
111
+ return {"id": item_id, **items_db[item_id]}
112
+
113
+@app.get("/items")
114
+def list_items():
115
+ return [{"id": k, **v} for k, v in items_db.items()]
116
+
117
+@app.delete("/items/{item_id}", status_code=204)
118
+def delete_item(item_id: int):
119
+ if item_id not in items_db:
120
+ raise HTTPException(status_code=404, detail="Not found")
121
+ del items_db[item_id]
122
+```
123
+
124
+Tests:
125
+
126
+```python
127
+client = JustAPITestClient(app)
128
+
129
+def test_create_and_get():
130
+ # Create
131
+ response = client.post("/items", body={"name": "Widget", "price": 9.99})
132
+ assert response.status_code == 201
133
+ item_id = response.json()["id"]
134
+
135
+ # Get
136
+ response = client.get(f"/items/{item_id}")
137
+ assert response.status_code == 200
138
+ assert response.json()["name"] == "Widget"
139
+
140
+def test_list_items():
141
+ response = client.get("/items")
142
+ assert response.status_code == 200
143
+ assert isinstance(response.json(), list)
144
+
145
+def test_get_nonexistent():
146
+ response = client.get("/items/99999")
147
+ assert response.status_code == 404
148
+
149
+def test_delete_item():
150
+ # Create first
151
+ response = client.post("/items", body={"name": "To Delete"})
152
+ item_id = response.json()["id"]
153
+
154
+ # Delete
155
+ response = client.delete(f"/items/{item_id}")
156
+ assert response.status_code == 204
157
+
158
+ # Confirm deleted
159
+ response = client.get(f"/items/{item_id}")
160
+ assert response.status_code == 404
161
+```
162
+
77
163
## Running Tests
78
164
79
165
```bash
85
171
86
172
# Single test file
87
173
pytest tests/test_app.py -v
174
+
175
+# Run with coverage
176
+pytest --cov=myapp tests/
177
+```
178
+
179
+## Test File Layout
180
+
181
+```
182
+tests/
183
+├── test_items.py
184
+├── test_users.py
185
+├── test_auth.py
186
+└── conftest.py
88
187
```
89
188
90
189
## See Also
91
190
92
191
- [Testing Client API](/api-reference/testing-client/) — full client reference
93
192
- [Debugging](/tutorials/debugging/) — debugging techniques
94
-- [Testing Guide](/contributing/testing-guide/) — writing and running tests
193
+- [Testing a Database](/how-to/testing-database/) — database testing patterns
95
194