Front-End / Back-End Separated Calculator System — First Assignment
Course: Software Engineering, Fuzhou University (2601_FZU-MU_SE)
Student ID: 832401218
Assignment: First Assignment — Front-End and Back-End Separation Calculator System
1. Project Overview
This assignment requires afront-end / back-end separatedcalculator system: the front end handles only the user interface and interaction, the back end handles expression parsing, evaluation, and history persistence, and the two sides communicate over HTTP/JSON APIs. The system must be deployed to a cloud server and publicly accessible.
Features I implemented:
- Arithmetic with operator precedence, parentheses, decimals, and unary +/- (e.g.
-5,3 * -2) - Invalid-expression detection with user-friendly error messages (division by zero, missing operands, illegal characters, etc.)
- Calculation history: display, per-record deletion, and clear-all
- History persisted in an SQLite database —records survive a service restart
- Two fully separated GitHub repositories (front end / back end), deployed to the cloud and publicly accessible
2. Code Repositories
| Item | Link |
|---|---|
| Front-end repository | https://github.com/WJ4F5DA2/832401218_calculator_frontend |
| Back-end repository | https://github.com/WJ4F5DA2/832401218_calculator_backend |
| Front-end code style | https://github.com/WJ4F5DA2/832401218_calculator_frontend/blob/main/codestyle.md |
| Back-end code style | https://github.com/WJ4F5DA2/832401218_calculator_backend/blob/main/codestyle.md |
All code, README files, comments, and UI text in both repositories are written in English, and so is this blog post.
3. PSP Table
| Module | Estimated (min) | Actual (min) |
|---|---|---|
| Requirements analysis | 10 | 15 |
| System design | 15 | 20 |
| Front-end development | 30 | 35 |
| Back-end development | 30 | 45 |
| Expression evaluation module | 20 | 25 |
| Database design | 10 | 15 |
| History module | 15 | 20 |
| Front-end / back-end integration | 20 | 30 |
| Testing | 15 | 25 |
| Deployment | 30 | 90 |
| Blog writing | 30 | 45 |
| Total | 225 | 365 |
Deployment took far longer than estimated: the platform I chose first (Render) now requires every user to add a credit or debit card (Visa/MasterCard only, UnionPay not accepted) before the first deployment, so I switched to PythonAnywhere, which needs no payment method at all, and went through the deployment flow again.
4. Tech Stack and Functional Structure
Tech stack
| Layer | Technology |
|---|---|
| Front end | Plain HTML5 + CSS3 + JavaScript (no framework, no build step), Fetch API |
| Back end | Python 3.10 + Flask 3.x |
| Database | SQLite (Python standard librarysqlite3, zero extra dependencies) |
| Expression parsing | Hand-written tokenizer + recursive-descent parser (no eval/exec) |
| Deployment | PythonAnywhere free tier (WSGI + static files mapping) |
Functional structure diagram
Front-End / Back-End Separated Calculator System ├── Front end (832401218_calculator_frontend, pure static pages) │ ├── Calculator keypad │ │ ├── Digits 0-9, decimal point │ │ ├── Operators + - * /, parentheses ( ) │ │ ├── Backspace and Clear (C) │ │ └── Equals (=): calls POST /api/calculate │ ├── Expression display / result display / error message area │ └── History panel │ ├── Display history (GET /api/history, newest first) │ ├── Delete one record (DELETE /api/history/{id}) │ ├── Clear all (DELETE /api/history) │ └── Manual refresh └── Back end (832401218_calculator_backend, Flask + SQLite) ├── Controller layer: HTTP routes /api/*, JSON in/out ├── Service layer │ ├── Expression tokenizing + recursive-descent parsing + evaluation │ └── Calculation and history business logic, error wrapping └── Model layer: SQLite connection, table creation, CRUDArchitecture notes:the two sides communicate only through JSON APIs. The front end sends the raw expression string to the back end as-is; the back end parses and evaluates it, stores the record in the database, and returns the result as JSON; the front end only renders what it receives. The front end containsno calculation logic at all, which keeps the separation boundary sharp and also means validation cannot be bypassed by tampering with the front end — all validation happens on the back end.
5. Key Code Walkthrough
5.1 Expression evaluation: hand-written tokenizer + recursive-descent parser (calculator_service.py)
The assignment explicitly forbidseval(), so I implemented the classic two-phase approach from compiler fundamentals.
Tokenizing:the expression string is split into a token stream; illegal characters are rejected right here:
deftokenize(expression):"""Split an expression string into a list of tokens."""tokens=[]i=0length=len(expression)whilei<length:ch=expression[i]ifch.isspace():i+=1continueifch.isdigit()orch==".":start=iwhilei<lengthand(expression[i].isdigit()orexpression[i]=="."):i+=1number=expression[start:i]ifnumber.count(".")>1:raiseExpressionError("Invalid number: %s"%number)tokens.append(Token("num",number))elifchin"+-*/":tokens.append(Token("op",ch))i+=1elifch=="(":tokens.append(Token("lparen",ch))i+=1elifch==")":tokens.append(Token("rparen",ch))i+=1else:raiseExpressionError("Invalid character: %s"%ch)ifnottokens:raiseExpressionError("Empty expression")returntokensRecursive-descent parsing:four grammar rules map one-to-one onto four methods; operator precedence and associativity fall out of the grammar structure itself (exprhandles +/-,termhandles * and /,factorhandles unary signs,primaryhandles numbers and parentheses):
expr := term (("+" | "-") term)* term := factor (("*" | "/") factor)* factor := ("+" | "-") factor | primary primary := NUMBER | "(" expr ")"classParser:"""Recursive-descent parser for arithmetic expressions."""defparse(self):value=self._parse_expr()ifself.pos!=len(self.tokens):raiseExpressionError("Unexpected token: %s"%self._peek().value)returnvaluedef_parse_term(self):value=self._parse_factor()whileself._peek()isnotNoneandself._peek().kind=="op"and(self._peek().valuein"*/"):op=self.tokens[self.pos].value self.pos+=1right=self._parse_factor()ifop=="*":value=value*rightelse:ifright==0:raiseExpressionError("Division by zero")value=value/rightreturnvalueDivision by zero raisesExpressionError("Division by zero")during evaluation, which the route layer converts into a 400 response for the front end. Results are formatted so integers drop the trailing.0(9.0→9) to avoid redundant output.
5.2 API design (controller/routes.py)
Flask Blueprint mounted under the/apiprefix; all requests and responses are JSON, and errors follow a uniform shape{success: false, message: "..."}:
bp=Blueprint("api",__name__,url_prefix="/api")@bp.route("/calculate",methods=["POST"])defcalculate_route():"""Evaluate an expression sent by the front end and store the record."""body=request.get_json(silent=True)or{}expression=body.get("expression")try:record=calculate(expression)exceptCalculationErrorasexc:returnjsonify({"success":False,"message":exc.message}),400returnjsonify({"success":True,"expression":record["expression"],"result":record["result"],"id":record["id"],"created_at":record["created_at"],}),200API overview:
| Method | Path | Description |
|---|---|---|
| GET | /api/health | Health check |
| POST | /api/calculate | Evaluate an expression and store the record |
| GET | /api/history | Return all history records (newest first) |
| DELETE | /api/history/{id} | Delete one record (404 if it does not exist) |
| DELETE | /api/history | Clear all history records (optional feature) |
For cross-origin access I did not pull in an extra dependency; instead, Flask’safter_requesthook sets CORS headers manually so the front end can be hosted on any domain:
@app.after_requestdefadd_cors_headers(response):response.headers["Access-Control-Allow-Origin"]="*"response.headers["Access-Control-Allow-Headers"]="Content-Type"response.headers["Access-Control-Allow-Methods"]="GET, POST, DELETE, OPTIONS"returnresponse5.3 Database operations (model/database.py)
The SQLite database file lives in the project root; the table is created automatically on first start, so no manual setup is needed:
definit_db():"""Create the calculation history table if it does not exist."""conn=get_connection()try:conn.execute(""" CREATE TABLE IF NOT EXISTS calculation_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, expression TEXT NOT NULL, result TEXT NOT NULL, created_at TEXT NOT NULL ) """)conn.commit()finally:conn.close()All SQL uses parameterized binding (?placeholders), which eliminates SQL injection; the query orders byid DESC, which directly satisfies the newest-first display requirement:
deffetch_all_records():"""Return all history records, newest first."""conn=get_connection()try:rows=conn.execute("SELECT id, expression, result, created_at"" FROM calculation_history ORDER BY id DESC").fetchall()return[dict(row)forrowinrows]finally:conn.close()6. Deployment and Access
Live application: https://wj4f5da2.pythonanywhere.com/
Both ends are deployed on the PythonAnywhere free tier:
- Back end:a PythonAnywhere web app served through WSGI. After uploading the code to the server, the WSGI configuration file only needs a few lines to mount the Flask application:
importsys path="/home/wj4f5da2/832401218_calculator_backend"ifpathnotinsys.path:sys.path.insert(0,path)fromsrc.appimportcreate_app application=create_app()- Front end:a PythonAnywhere static files mapping points the site root
/at the front end’ssrcdirectory, so the server serves the static assets directly; requests to/api/*match no static file and fall through to WSGI, where Flask handles them. The whole application is therefore exposed under a single domain, while the front end and back end remain fully separated in code, repositories, and responsibilities. - The front end’s
API_BASEpoints tohttps://wj4f5da2.pythonanywhere.com/api.
Why PythonAnywhere: completely free, requires no credit/debit card at all, natively supports Flask + SQLite, and web apps run permanently without sleeping — a reliable option for a student assignment.
Testing notes:a free-tier web app is valid for 3 months; PythonAnywhere emails a reminder before expiry, and renewal takes a few clicks in the console. If the application is unreachable during grading, it has usually been stopped manually — logging into the console and clicking “Reload” brings it back. Instructions for running locally are in each repository’s README (python run.pyfor the back end, any static file server for the front end).
7. Screenshots
All screenshots were taken from the running system. The UI is in English, consistent with the project’s language.
【Figure 1: 01-basic-add.png】Basic addition: enter12 + 8, press=, the result= 20is shown and a history record is added automatically.
【Figure 2: 02-compound.png】Mixed operators and precedence:(1+2)*3 = 9— parentheses first, then multiplication.
【Figure 3: 03-decimal.png】Decimal arithmetic:10 / 4 = 2.5.
【Figure 4: 04-unary.png】Unary minus:-5 + 8 = 3; expressions like3 * -2are also supported.
【Figure 5: 05-div-zero.png】Division by zero:1/0reports “Division by zero” and no history record is created.
【Figure 6: 06-invalid.png】Invalid expression:1++(a trailing unary plus with no operand) reports “Unexpected end of expression”.
【Figure 7: 07-history.png】History panel: every successful calculation is appended automatically, displayed newest first, and each record has a delete button.
【Figure 8: 08-delete.png】Per-record deletion: clicking×on a record removes only that record; the rest are kept.
【Figure 9: 09-persistence.png】Persistence check: after calculating5 × 8 = 40andrefreshing the page, all 4 records (including 5×8=40) are still there — the data lives in the back-end SQLite database.
【Figure 10: 10-backend-down.png】Separation proof: with theback end stopped, pressing=resets the result to zero and reports “Failed to fetch”, and the history panel shows “Cannot connect to the back end” — calculation really happens on the back end, and the front end cannot produce a result on its own.
【Figure 11: 11-clear-all.png】Clear all: after clicking “Clear”, the history is empty and shows “No history records”.
8. Testing
The back end ships withtest_api.py, an API smoke-test suite (Flask test client) covering addition/subtraction/multiplication/division, precedence and parentheses, unary signs, decimals, invalid expressions, division by zero, and history insert/query/delete/clear — 30 assertions in total, all passing. Front-end / back-end integration was verified in both directions (UI operations plus back-end logs), including the failure scenario “no result when the back end is down” (Figure 10).
9. Personal Summary
- Separation is about boundaries.The key is resisting the urge to “just calculate it in the front end”: all computation, validation, and persistence belong on the back end, while the front end only sends requests and renders responses. This lets both sides be developed, tested, and deployed independently.
- Writing a parser by hand (no eval) was very rewarding.Tokenizing + recursive descent was the first time classroom theory really clicked for me: four grammar rules handle precedence, associativity, unary operators, and parentheses — more elegant than I expected.
- Deployment was the biggest lesson.Free-tier policies of cloud platforms change without notice (Render went from no-card-required to mandatory card binding), so always confirm payment requirements before choosing a platform; PythonAnywhere turned out to be a solid, dependable option for a student assignment.
- Engineering hygiene matters too.Writing README files, a codestyle document, English comments, and keeping the whole project in English took real time — but it makes the repositories presentable and trained my engineering communication skills.