├── .bumpversion.cfg ├── .flaskenv ├── .gitignore ├── .travis.yml ├── HISTORY.md ├── LICENSE.md ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── VERSION ├── app.json ├── app ├── __init__.py ├── api │ ├── __init__.py │ ├── resources.py │ └── security.py ├── client.py └── config.py ├── docs ├── flask-logo.png ├── project-logo.png ├── python-logo.png └── vue-logo.png ├── package.json ├── public ├── favicon.ico └── index.html ├── run.py ├── src ├── App.vue ├── assets │ ├── flask-logo.png │ ├── logo.png │ ├── python-logo.png │ └── vue-logo.png ├── backend.js ├── components │ └── HelloWorld.vue ├── filters.js ├── main.js ├── router.js ├── store.js └── views │ ├── Api.vue │ └── Home.vue ├── tests ├── __init__.py ├── test_api.py └── test_client.py ├── tox.ini ├── vue.config.js └── yarn.lock /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.3.0.post1 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:VERSION] 7 | 8 | -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | # Production Enviroment should be set to 'production' 2 | FLASK_ENV = "development" 3 | FLASK_APP = "app" 4 | # Uncomment this to debug: 5 | # FLASK_DEBUG=1 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .cache 3 | .mypy_cache\ 4 | .env 5 | 6 | /dist 7 | .coverage 8 | .pytest_cache/ 9 | htmlcov/ 10 | 11 | .DS_Store 12 | node_modules/ 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | selenium-debug.log 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *security-sgps.py 26 | *security_backend.py 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | git: 3 | depth: 2 4 | python: 5 | # - 3.5 6 | - 3.6 7 | branches: 8 | only: 9 | - master 10 | - dev/main 11 | script: 12 | - pytest --cov=app --cov-report=xml 13 | install: 14 | - "pip install --upgrade pip" 15 | - "pip install pipenv" 16 | - "pipenv install --dev" 17 | - "pip install codecov" 18 | after_success: 19 | - codecov 20 | cache: yarn 21 | before_install: 22 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.9.4 23 | - export PATH="$HOME/.yarn/bin:$PATH" 24 | - yarn install 25 | - yarn build 26 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ### 0.3.0.post1 2 | * Updated Dependencies and Docs (#17) 3 | 4 | ### 0.3.0 5 | * Npm > Yarn 6 | * Updated to Flask 1.0 7 | * Clean up config, use env vars 8 | * Simplified client view 9 | * Flatten Templated Folder Structure 10 | * Removed Cli Commads 11 | 12 | ### 0.2.3 13 | * Updated to Vue Cli 3 14 | 15 | ### 0.1.0 16 | * Initial Release 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Gui Talarico] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | gunicorn = "==19.7.1" 8 | flask-restplus = "*" 9 | python-dotenv = "*" 10 | flask = "*" 11 | 12 | [dev-packages] 13 | pytest = "*" 14 | bumpversion = "*" 15 | pytest-sugar = "*" 16 | pytest-cov = "*" 17 | 18 | [requires] 19 | python_version = "3.6" 20 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "86fcf75e89c8bcab95ada34b3d66e96a8fbb94949d95bf087307aa2acbd42312" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aniso8601": { 20 | "hashes": [ 21 | "sha256:547e7bc88c19742e519fb4ca39f4b8113fdfb8fca322e325f16a8bfc6cfc553c", 22 | "sha256:e7560de91bf00baa712b2550a2fdebf0188c5fce2fcd1162fbac75c19bb29c95" 23 | ], 24 | "version": "==4.0.1" 25 | }, 26 | "click": { 27 | "hashes": [ 28 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 29 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 30 | ], 31 | "version": "==7.0" 32 | }, 33 | "flask": { 34 | "hashes": [ 35 | "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", 36 | "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" 37 | ], 38 | "index": "pypi", 39 | "version": "==1.0.2" 40 | }, 41 | "flask-restplus": { 42 | "hashes": [ 43 | "sha256:3fad697e1d91dfc13c078abcb86003f438a751c5a4ff41b84c9050199d2eab62", 44 | "sha256:cdc27b5be63f12968a7f762eaa355e68228b0c904b4c96040a314ba7dc6d0e69" 45 | ], 46 | "index": "pypi", 47 | "version": "==0.12.1" 48 | }, 49 | "gunicorn": { 50 | "hashes": [ 51 | "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6", 52 | "sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622" 53 | ], 54 | "index": "pypi", 55 | "version": "==19.7.1" 56 | }, 57 | "itsdangerous": { 58 | "hashes": [ 59 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 60 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 61 | ], 62 | "version": "==1.1.0" 63 | }, 64 | "jinja2": { 65 | "hashes": [ 66 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", 67 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" 68 | ], 69 | "version": "==2.10" 70 | }, 71 | "jsonschema": { 72 | "hashes": [ 73 | "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", 74 | "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02" 75 | ], 76 | "version": "==2.6.0" 77 | }, 78 | "markupsafe": { 79 | "hashes": [ 80 | "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", 81 | "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", 82 | "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", 83 | "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", 84 | "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", 85 | "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", 86 | "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", 87 | "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", 88 | "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", 89 | "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", 90 | "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", 91 | "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", 92 | "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", 93 | "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", 94 | "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", 95 | "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", 96 | "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", 97 | "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", 98 | "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", 99 | "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", 100 | "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", 101 | "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", 102 | "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", 103 | "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", 104 | "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", 105 | "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", 106 | "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", 107 | "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" 108 | ], 109 | "version": "==1.1.0" 110 | }, 111 | "python-dotenv": { 112 | "hashes": [ 113 | "sha256:122290a38ece9fe4f162dc7c95cae3357b983505830a154d3c98ef7f6c6cea77", 114 | "sha256:4a205787bc829233de2a823aa328e44fd9996fedb954989a21f1fc67c13d7a77" 115 | ], 116 | "index": "pypi", 117 | "version": "==0.9.1" 118 | }, 119 | "pytz": { 120 | "hashes": [ 121 | "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca", 122 | "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6" 123 | ], 124 | "version": "==2018.7" 125 | }, 126 | "six": { 127 | "hashes": [ 128 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 129 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 130 | ], 131 | "version": "==1.11.0" 132 | }, 133 | "werkzeug": { 134 | "hashes": [ 135 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", 136 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" 137 | ], 138 | "version": "==0.14.1" 139 | } 140 | }, 141 | "develop": { 142 | "atomicwrites": { 143 | "hashes": [ 144 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", 145 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" 146 | ], 147 | "version": "==1.2.1" 148 | }, 149 | "attrs": { 150 | "hashes": [ 151 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 152 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 153 | ], 154 | "version": "==18.2.0" 155 | }, 156 | "bumpversion": { 157 | "hashes": [ 158 | "sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e", 159 | "sha256:6753d9ff3552013e2130f7bc03c1007e24473b4835952679653fb132367bdd57" 160 | ], 161 | "index": "pypi", 162 | "version": "==0.5.3" 163 | }, 164 | "coverage": { 165 | "hashes": [ 166 | "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", 167 | "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", 168 | "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", 169 | "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", 170 | "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", 171 | "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", 172 | "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", 173 | "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", 174 | "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", 175 | "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", 176 | "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", 177 | "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", 178 | "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", 179 | "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", 180 | "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", 181 | "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", 182 | "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", 183 | "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", 184 | "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", 185 | "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", 186 | "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", 187 | "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", 188 | "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", 189 | "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", 190 | "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", 191 | "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", 192 | "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", 193 | "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", 194 | "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", 195 | "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", 196 | "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" 197 | ], 198 | "version": "==4.5.2" 199 | }, 200 | "more-itertools": { 201 | "hashes": [ 202 | "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", 203 | "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", 204 | "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" 205 | ], 206 | "version": "==4.3.0" 207 | }, 208 | "packaging": { 209 | "hashes": [ 210 | "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", 211 | "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" 212 | ], 213 | "version": "==18.0" 214 | }, 215 | "pluggy": { 216 | "hashes": [ 217 | "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", 218 | "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" 219 | ], 220 | "version": "==0.8.0" 221 | }, 222 | "py": { 223 | "hashes": [ 224 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", 225 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" 226 | ], 227 | "version": "==1.7.0" 228 | }, 229 | "pyparsing": { 230 | "hashes": [ 231 | "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", 232 | "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" 233 | ], 234 | "version": "==2.3.0" 235 | }, 236 | "pytest": { 237 | "hashes": [ 238 | "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec", 239 | "sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660" 240 | ], 241 | "index": "pypi", 242 | "version": "==3.10.1" 243 | }, 244 | "pytest-cov": { 245 | "hashes": [ 246 | "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", 247 | "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762" 248 | ], 249 | "index": "pypi", 250 | "version": "==2.6.0" 251 | }, 252 | "pytest-sugar": { 253 | "hashes": [ 254 | "sha256:26cf8289fe10880cbbc130bd77398c4e6a8b936d8393b116a5c16121d95ab283", 255 | "sha256:fcd87a74b2bce5386d244b49ad60549bfbc4602527797fac167da147983f58ab" 256 | ], 257 | "index": "pypi", 258 | "version": "==0.9.2" 259 | }, 260 | "six": { 261 | "hashes": [ 262 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 263 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 264 | ], 265 | "version": "==1.11.0" 266 | }, 267 | "termcolor": { 268 | "hashes": [ 269 | "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" 270 | ], 271 | "version": "==1.1.0" 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app --log-file - 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-VueJs-Template 🌶️✌ 2 | 3 | [![Build Status](https://travis-ci.org/gtalarico/flask-vuejs-template.svg?branch=master)](https://travis-ci.org/gtalarico/flask-vuejs-template) 4 | [![codecov](https://codecov.io/gh/gtalarico/flask-vuejs-template/branch/master/graph/badge.svg)](https://codecov.io/gh/gtalarico/flask-vuejs-template) 5 | 6 | _Flask + Vue.js Web Application Template_ 7 | 8 | ![Vue Logo](/docs/vue-logo.png "Vue Logo") ![Flask Logo](/docs/flask-logo.png "Flask Logo") 9 | 10 | ## Features 11 | * Minimal Flask 1.0 App 12 | * [Flask-RestPlus](http://flask-restplus.readthedocs.io) API with class-based secure resource routing 13 | * Starter [PyTest](http://pytest.org) test suite 14 | * [vue-cli 3](https://github.com/vuejs/vue-cli/blob/dev/docs/README.md) + yarn 15 | * [Vuex](https://vuex.vuejs.org/) 16 | * [Vue Router](https://router.vuejs.org/) 17 | * [Axios](https://github.com/axios/axios/) for backend communication 18 | * Sample Vue [Filters](https://vuejs.org/v2/guide/filters.html) 19 | * Heroku Configuration with one-click deployment + Gunicorn 20 | 21 | ## Demo 22 | [Live Demo](https://flask-vuejs-template.herokuapp.com/#/api) 23 | 24 | ## Alternatives 25 | 26 | If this setup is not what you are looking for, here are some similar projects: 27 | 28 | * [oleg-agapov/flask-vue-spa](https://github.com/oleg-agapov/flask-vue-spa) 29 | * [testdrivenio/flask-vue-crud](https://github.com/testdrivenio/flask-vue-crud) 30 | 31 | #### Old Template 32 | 33 | This template was updated to use a flatter folder structure and use yarn instead of npm. 34 | You can now run `yarn serve` as well as other yarn commands from the template root directory. 35 | The old template will be kept in the [npm-template branch](https://github.com/gtalarico/flask-vuejs-template/tree/npm-template) but will not be maintained. 36 | 37 | #### Django 38 | 39 | Prefer Django? Checkout the [gtalarico/django-vue-template](https://github.com/gtalarico/django-vue-template) 40 | 41 | ## Template Structure 42 | 43 | The template uses Flask & Flask-RestPlus to create a minimal REST style API, 44 | and let's VueJs + vue-cli handle the front end and asset pipline. 45 | Data from the python server to the Vue application is passed by making Ajax requests. 46 | 47 | ### Application Structure 48 | 49 | #### Rest Api 50 | 51 | The Api is served using a Flask blueprint at `/api/` using Flask RestPlus class-based 52 | resource routing. 53 | 54 | #### Client Application 55 | 56 | A Flask view is used to serve the `index.html` as an entry point into the Vue app at the endpoint `/`. 57 | 58 | The template uses vue-cli 3 and assumes Vue Cli & Webpack will manage front-end resources and assets, so it does overwrite template delimiter. 59 | 60 | The Vue instance is preconfigured with Filters, Vue-Router, Vuex; each of these can easilly removed if they are not desired. 61 | 62 | #### Important Files 63 | 64 | | Location | Content | 65 | |----------------------|--------------------------------------------| 66 | | `/app` | Flask Application | 67 | | `/app/api` | Flask Rest Api (`/api`) | 68 | | `/app/client.py` | Flask Client (`/`) | 69 | | `/src` | Vue App . | 70 | | `/src/main.js` | JS Application Entry Point | 71 | | `/public/index.html` | Html Application Entry Point (`/`) | 72 | | `/public/static` | Static Assets | 73 | | `/dist/` | Bundled Assets Output (generated at `yarn build` | 74 | 75 | 76 | ## Installation 77 | 78 | ##### Before you start 79 | 80 | Before getting started, you should have the following installed and running: 81 | 82 | - [X] Yarn - [instructions](https://yarnpkg.com/en/docs/install#mac-stable) 83 | - [X] Vue Cli 3 - [instructions](https://cli.vuejs.org/guide/installation.html) 84 | - [X] Python 3 85 | - [X] Pipenv (optional) 86 | - [X] Heroku Cli (if deploying to Heroku) 87 | 88 | ##### Template and Dependencies 89 | 90 | * Clone this repository: 91 | 92 | ``` 93 | $ git clone https://github.com/gtalarico/flask-vuejs-template.git 94 | ``` 95 | 96 | * Setup virtual environment, install dependencies, and activate it: 97 | 98 | ``` 99 | $ pipenv install --dev 100 | $ pipenv shell 101 | ``` 102 | 103 | * Install JS dependencies 104 | 105 | ``` 106 | $ yarn install 107 | ``` 108 | 109 | 110 | ## Development Server 111 | 112 | Run Flask Api development server: 113 | 114 | ``` 115 | $ python run.py 116 | ``` 117 | 118 | From another tab in the same directory, start the webpack dev server: 119 | 120 | ``` 121 | $ yarn serve 122 | ``` 123 | 124 | The Vuejs application will be served from `localhost:8080` and the Flask Api 125 | and static files will be served from `localhost:5000`. 126 | 127 | The dual dev-server setup allows you to take advantage of 128 | webpack's development server with hot module replacement. 129 | 130 | Proxy config in `vue.config.js` is used to route the requests 131 | back to Flask's Api on port 5000. 132 | 133 | If you would rather run a single dev server, you can run Flask's 134 | development server only on `:5000`, but you have to build build the Vue app first 135 | and the page will not reload on changes. 136 | 137 | ``` 138 | $ yarn build 139 | $ python run.py 140 | ``` 141 | 142 | 143 | ## Production Server 144 | 145 | This template is configured to work with Heroku + Gunicorn and it's pre-configured 146 | to have Heroku build the application before releasing it. 147 | 148 | #### JS Build Process 149 | 150 | Heroku's nodejs buidlpack will handle install for all the dependencies from the `packages.json` file. 151 | It will then trigger the `postinstall` command which calls `yarn build`. 152 | This will create the bundled `dist` folder which will be served by whitenoise. 153 | 154 | #### Python Build Process 155 | 156 | The python buildpack will detect the `Pipfile` and install all the python dependencies. 157 | 158 | #### Production Sever Setup 159 | 160 | Here are the commands we need to run to get things setup on the Heroku side: 161 | 162 | ``` 163 | $ heroku apps:create flask-vuejs-template-demo 164 | $ heroku git:remote --app flask-vuejs-template-demo 165 | $ heroku buildpacks:add --index 1 heroku/nodejs 166 | $ heroku buildpacks:add --index 2 heroku/python 167 | $ heroku config:set FLASK_ENV=production 168 | $ heroku config:set FLASK_SECRET=SuperSecretKey 169 | 170 | $ git push heroku 171 | ``` 172 | 173 | ### Heroku deployment - One Click Deploy 174 | 175 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/gtalarico/flask-vuejs-template) 176 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "Flask VueJs Template", 4 | "description": "", 5 | "repository": "https://github.com/gtalarico/flask-vuejs-template", 6 | "logo": "https://github.com/gtalarico/flask-vuejs-template/raw/master/docs/project-logo.png", 7 | "keywords": ["flask", "vue"], 8 | "env": { 9 | "FLASK_ENV": { 10 | "description": "Flask Enviroment", 11 | "value": "production" 12 | }, 13 | "SECRET": { 14 | "description": "Flask Secret Key", 15 | "value": "YourKeyHere" 16 | } 17 | }, 18 | "addons": [ 19 | ], 20 | "buildpacks": [ 21 | { 22 | "url": "heroku/nodejs" 23 | }, 24 | { 25 | "url": "heroku/python" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, current_app, send_file 3 | 4 | from .api import api_bp 5 | from .client import client_bp 6 | 7 | app = Flask(__name__, static_folder='../dist/static') 8 | app.register_blueprint(api_bp) 9 | # app.register_blueprint(client_bp) 10 | 11 | from .config import Config 12 | app.logger.info('>>> {}'.format(Config.FLASK_ENV)) 13 | 14 | @app.route('/') 15 | def index_client(): 16 | dist_dir = current_app.config['DIST_DIR'] 17 | entry = os.path.join(dist_dir, 'index.html') 18 | return send_file(entry) 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | """ API Blueprint Application """ 2 | 3 | from flask import Blueprint, current_app 4 | from flask_restplus import Api 5 | 6 | api_bp = Blueprint('api_bp', __name__, url_prefix='/api') 7 | api_rest = Api(api_bp) 8 | 9 | 10 | @api_bp.after_request 11 | def add_header(response): 12 | response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization' 13 | return response 14 | 15 | 16 | # Import resources to ensure view is registered 17 | from .resources import * # NOQA 18 | -------------------------------------------------------------------------------- /app/api/resources.py: -------------------------------------------------------------------------------- 1 | """ 2 | REST API Resource Routing 3 | http://flask-restplus.readthedocs.io 4 | """ 5 | 6 | from datetime import datetime 7 | from flask import request 8 | from flask_restplus import Resource 9 | 10 | from .security import require_auth 11 | from . import api_rest 12 | 13 | 14 | class SecureResource(Resource): 15 | """ Calls require_auth decorator on all requests """ 16 | method_decorators = [require_auth] 17 | 18 | 19 | @api_rest.route('/resource/') 20 | class ResourceOne(Resource): 21 | """ Unsecure Resource Class: Inherit from Resource """ 22 | 23 | def get(self, resource_id): 24 | timestamp = datetime.utcnow().isoformat() 25 | return {'timestamp': timestamp} 26 | 27 | def post(self, resource_id): 28 | json_payload = request.json 29 | return {'timestamp': json_payload}, 201 30 | 31 | 32 | @api_rest.route('/secure-resource/') 33 | class SecureResourceOne(SecureResource): 34 | """ Unsecure Resource Class: Inherit from Resource """ 35 | 36 | def get(self, resource_id): 37 | timestamp = datetime.utcnow().isoformat() 38 | return {'timestamp': timestamp} 39 | -------------------------------------------------------------------------------- /app/api/security.py: -------------------------------------------------------------------------------- 1 | """ Security Related things """ 2 | from functools import wraps 3 | from flask import request 4 | from flask_restplus import abort 5 | 6 | 7 | def require_auth(func): 8 | """ Secure method decorator """ 9 | @wraps(func) 10 | def wrapper(*args, **kwargs): 11 | # Verify if User is Authenticated 12 | # Authentication logic goes here 13 | if request.headers.get('authorization'): 14 | return func(*args, **kwargs) 15 | else: 16 | return abort(401) 17 | return wrapper 18 | -------------------------------------------------------------------------------- /app/client.py: -------------------------------------------------------------------------------- 1 | """ Client App """ 2 | 3 | import os 4 | from flask import Blueprint, render_template 5 | 6 | client_bp = Blueprint('client_app', __name__, 7 | url_prefix='', 8 | static_url_path='', 9 | static_folder='./dist/static/', 10 | template_folder='./dist/', 11 | ) 12 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Global Flask Application Setting 3 | 4 | See `.flaskenv` for default settings. 5 | """ 6 | 7 | import os 8 | from app import app 9 | 10 | 11 | class Config(object): 12 | # If not set fall back to production for safety 13 | FLASK_ENV = os.getenv('FLASK_ENV', 'production') 14 | # Set FLASK_SECRET on your production Environment 15 | SECRET_KEY = os.getenv('FLASK_SECRET', 'Secret') 16 | 17 | APP_DIR = os.path.dirname(__file__) 18 | ROOT_DIR = os.path.dirname(APP_DIR) 19 | DIST_DIR = os.path.join(ROOT_DIR, 'dist') 20 | 21 | if not os.path.exists(DIST_DIR): 22 | raise Exception( 23 | 'DIST_DIR not found: {}'.format(DIST_DIR)) 24 | 25 | app.config.from_object('app.config.Config') 26 | -------------------------------------------------------------------------------- /docs/flask-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/docs/flask-logo.png -------------------------------------------------------------------------------- /docs/project-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/docs/project-logo.png -------------------------------------------------------------------------------- /docs/python-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/docs/python-logo.png -------------------------------------------------------------------------------- /docs/vue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/docs/vue-logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue_app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --open", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "postinstall": "yarn build" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.18.0", 13 | "vue": "^2.5.13", 14 | "vue-router": "^3.0.1", 15 | "vuex": "^3.0.1" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^3.0.0-beta.6", 19 | "@vue/cli-plugin-eslint": "^3.0.0-beta.6", 20 | "@vue/cli-service": "^3.0.0-beta.6", 21 | "@vue/eslint-config-standard": "^3.0.0-beta.6", 22 | "lint-staged": "^6.0.0", 23 | "node-sass": "^4.7.2", 24 | "sass-loader": "^6.0.6", 25 | "vue-template-compiler": "^2.5.13" 26 | }, 27 | "babel": { 28 | "presets": [ 29 | "@vue/app" 30 | ] 31 | }, 32 | "eslintConfig": { 33 | "root": true, 34 | "extends": [ 35 | "plugin:vue/essential", 36 | "@vue/standard" 37 | ] 38 | }, 39 | "postcss": { 40 | "plugins": { 41 | "autoprefixer": {} 42 | } 43 | }, 44 | "browserslist": [ 45 | "> 1%", 46 | "last 2 versions", 47 | "not ie <= 8" 48 | ], 49 | "gitHooks": { 50 | "pre-commit": "lint-staged" 51 | }, 52 | "lint-staged": { 53 | "*.js": [ 54 | "vue-cli-service lint", 55 | "git add" 56 | ], 57 | "*.vue": [ 58 | "vue-cli-service lint", 59 | "git add" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Flask + Vue.js Template 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | from app import app 3 | 4 | app.run(port=5000) 5 | 6 | # To Run: 7 | # python run.py 8 | # or 9 | # python -m flask run 10 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 49 | -------------------------------------------------------------------------------- /src/assets/flask-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/src/assets/flask-logo.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/python-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/src/assets/python-logo.png -------------------------------------------------------------------------------- /src/assets/vue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/src/assets/vue-logo.png -------------------------------------------------------------------------------- /src/backend.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | let $axios = axios.create({ 4 | baseURL: '/api/', 5 | timeout: 5000, 6 | headers: {'Content-Type': 'application/json'} 7 | }) 8 | 9 | // Request Interceptor 10 | $axios.interceptors.request.use(function (config) { 11 | config.headers['Authorization'] = 'Fake Token' 12 | return config 13 | }) 14 | 15 | // Response Interceptor to handle and log errors 16 | $axios.interceptors.response.use(function (response) { 17 | return response 18 | }, function (error) { 19 | // Handle Error 20 | console.log(error) 21 | return Promise.reject(error) 22 | }) 23 | 24 | export default { 25 | 26 | fetchResource () { 27 | return $axios.get(`resource/xxx`) 28 | .then(response => response.data) 29 | }, 30 | 31 | fetchSecureResource () { 32 | return $axios.get(`secure-resource/zzz`) 33 | .then(response => response.data) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | 41 | 42 | 58 | -------------------------------------------------------------------------------- /src/filters.js: -------------------------------------------------------------------------------- 1 | // Vue.js Filters 2 | // https://vuejs.org/v2/guide/filters.html 3 | 4 | import Vue from 'vue' 5 | 6 | let filters = { 7 | 8 | formatTimestamp (timestamp) { 9 | let datetime = new Date(timestamp) 10 | return datetime.toLocaleTimeString('en-US') 11 | } 12 | } 13 | 14 | // Register All Filters on import 15 | Object.keys(filters).forEach(function (filterName) { 16 | Vue.filter(filterName, filters[filterName]) 17 | }) 18 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | 6 | import './filters' 7 | 8 | Vue.config.productionTip = false 9 | 10 | new Vue({ 11 | router, 12 | store, 13 | render: h => h(App) 14 | }).$mount('#app') 15 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | import Api from './views/Api.vue' 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | routes: [ 10 | { 11 | path: '/', 12 | name: 'home', 13 | component: Home 14 | }, 15 | { 16 | path: '/api', 17 | name: 'api', 18 | component: Api 19 | } 20 | ] 21 | }) 22 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | 9 | }, 10 | mutations: { 11 | 12 | }, 13 | actions: { 14 | 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/views/Api.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 48 | 49 | 51 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/5106acf6430e0d09d174d4f09109e903ee7d6128/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | """ pytests for Flask """ 2 | 3 | import pytest 4 | from app import app 5 | 6 | @pytest.fixture(scope="module") 7 | def client(): 8 | app.config['TESTING'] = True 9 | return app.test_client() 10 | 11 | def test_api(client): 12 | resp = client.get('/api/') 13 | assert resp.status_code == 200 14 | 15 | def test_resource_one(client): 16 | resp = client.get('/api/resource/one') 17 | assert resp.status_code == 200 18 | 19 | def test_resource_one_post(client): 20 | resp = client.post('/api/resource/one') 21 | assert resp.status_code == 201 22 | 23 | def test_resource_one_patch(client): 24 | resp = client.patch('/api/resource/one') 25 | assert resp.status_code == 405 26 | 27 | def test_secure_resource_fail(client): 28 | resp = client.get('/api/secure-resource/two') 29 | assert resp.status_code == 401 30 | 31 | def test_secure_resource_pass(client): 32 | resp = client.get('/api/secure-resource/two', 33 | headers={'authorization': 'Bearer x'}) 34 | assert resp.status_code == 200 35 | 36 | @pytest.fixture(scope="module") 37 | def request_context(): 38 | return app.test_request_context('') 39 | 40 | def test_session(request_context): 41 | with request_context: 42 | # Do something that requires request context 43 | assert True 44 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | """ pytests for Flask """ 2 | 3 | import pytest 4 | from app import app 5 | 6 | @pytest.fixture(scope="module") 7 | def client(): 8 | app.config['TESTING'] = True 9 | return app.test_client() 10 | 11 | def test_api(client): 12 | resp = client.get('/') 13 | assert resp.status_code == 200 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts=--maxfail=2 --cov app --cov-report=html 3 | testpaths=tests 4 | [testenv] 5 | deps=pytest 6 | commands=pytest 7 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | // const IS_PRODUCTION = process.env.NODE_ENV === 'production' 2 | 3 | module.exports = { 4 | outputDir: 'dist', 5 | assetsDir: 'static', 6 | // baseUrl: IS_PRODUCTION 7 | // ? 'http://cdn123.com' 8 | // : '/', 9 | // For Production, replace set baseUrl to CDN 10 | // And set the CDN origin to `yourdomain.com/static` 11 | // Whitenoise will serve once to CDN which will then cache 12 | // and distribute 13 | devServer: { 14 | proxy: { 15 | '/api*': { 16 | // Forward frontend dev server request for /api to flask dev server 17 | target: 'http://localhost:5000/' 18 | } 19 | } 20 | } 21 | } 22 | --------------------------------------------------------------------------------