832401218_calculator_blog Front-End / Back-End Separated Calculator System — First AssignmentCourse: Software Engineering, Fuzhou University (2601_FZU-MU_SE)Student ID: 832401218Assignment: First Assignment — Front-End and Back-End Separation Calculator System1. Project OverviewThis 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-allHistory persisted in an SQLite database —records survive a service restartTwo fully separated GitHub repositories (front end / back end), deployed to the cloud and publicly accessible2. Code RepositoriesItemLinkFront-end repositoryhttps://github.com/WJ4F5DA2/832401218_calculator_frontendBack-end repositoryhttps://github.com/WJ4F5DA2/832401218_calculator_backendFront-end code stylehttps://github.com/WJ4F5DA2/832401218_calculator_frontend/blob/main/codestyle.mdBack-end code stylehttps://github.com/WJ4F5DA2/832401218_calculator_backend/blob/main/codestyle.mdAll code, README files, comments, and UI text in both repositories are written in English, and so is this blog post.3. PSP TableModuleEstimated (min)Actual (min)Requirements analysis1015System design1520Front-end development3035Back-end development3045Expression evaluation module2025Database design1015History module1520Front-end / back-end integration2030Testing1525Deployment3090Blog writing3045Total225365Deployment 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 StructureTech stackLayerTechnologyFront endPlain HTML5 CSS3 JavaScript (no framework, no build step), Fetch APIBack endPython 3.10 Flask 3.xDatabaseSQLite (Python standard librarysqlite3, zero extra dependencies)Expression parsingHand-written tokenizer recursive-descent parser (no eval/exec)DeploymentPythonAnywhere free tier (WSGI static files mapping)Functional structure diagramFront-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 Walkthrough5.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[]i0lengthlen(expression)whileilength:chexpression[i]ifch.isspace():i1continueifch.isdigit()orch.:startiwhileilengthand(expression[i].isdigit()orexpression[i].):i1numberexpression[start:i]ifnumber.count(.)1:raiseExpressionError(Invalid number: %s%number)tokens.append(Token(num,number))elifchin-*/:tokens.append(Token(op,ch))i1elifch(:tokens.append(Token(lparen,ch))i1elifch):tokens.append(Token(rparen,ch))i1else: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):valueself._parse_expr()ifself.pos!len(self.tokens):raiseExpressionError(Unexpected token: %s%self._peek().value)returnvaluedef_parse_term(self):valueself._parse_factor()whileself._peek()isnotNoneandself._peek().kindopand(self._peek().valuein*/):opself.tokens[self.pos].value self.pos1rightself._parse_factor()ifop*:valuevalue*rightelse:ifright0:raiseExpressionError(Division by zero)valuevalue/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: ...}:bpBlueprint(api,__name__,url_prefix/api)bp.route(/calculate,methods[POST])defcalculate_route():Evaluate an expression sent by the front end and store the record.bodyrequest.get_json(silentTrue)or{}expressionbody.get(expression)try:recordcalculate(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:MethodPathDescriptionGET/api/healthHealth checkPOST/api/calculateEvaluate an expression and store the recordGET/api/historyReturn all history records (newest first)DELETE/api/history/{id}Delete one record (404 if it does not exist)DELETE/api/historyClear 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-Typeresponse.headers[Access-Control-Allow-Methods]GET, POST, DELETE, OPTIONSreturnresponse5.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.connget_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.connget_connection()try:rowsconn.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 AccessLive 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_backendifpathnotinsys.path:sys.path.insert(0,path)fromsrc.appimportcreate_app applicationcreate_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’sAPI_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. ScreenshotsAll 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:(12)*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×840) are still there — the data lives in the back-end SQLite database.【Figure 10: 10-backend-down.png】Separation proof: with theback end stopped, pressingresets 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. TestingThe 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 SummarySeparation 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.