diff --git a/app/src/api/api.ts b/app/src/api/api.ts
new file mode 100644
index 00000000..2ab9ba3b
--- /dev/null
+++ b/app/src/api/api.ts
@@ -0,0 +1,141 @@
+import express from 'express';
+import bodyParser from 'body-parser';
+
+import { authUser, createUser, getUserById, getNewUserAPIKey } from './services/user';
+import { queryLocation } from './services/search';
+
+import buildingsRouter from './routes/buildingsRouter';
+
+
+const server = express.Router()
+
+// parse POSTed json body
+server.use(bodyParser.json());
+
+server.use('/buildings', buildingsRouter);
+
+// POST new user
+server.post('/users', function (req, res) {
+ const user = req.body;
+ if (req.session.user_id) {
+ res.send({ error: 'Already signed in' });
+ return
+ }
+
+ if (user.email) {
+ if (user.email != user.confirm_email) {
+ res.send({ error: 'Email did not match confirmation.' });
+ return
+ }
+ } else {
+ user.email = null;
+ }
+
+ createUser(user).then(function (result) {
+ if (result.user_id) {
+ req.session.user_id = result.user_id;
+ res.send({ user_id: result.user_id });
+ } else {
+ req.session.user_id = undefined;
+ res.send({ error: result.error });
+ }
+ }).catch(function (err) {
+ console.error(err);
+ res.send(err)
+ });
+});
+
+// POST user auth
+server.post('/login', function (req, res) {
+ authUser(req.body.username, req.body.password).then(function (user: any) { // TODO: remove any
+ if (user.user_id) {
+ req.session.user_id = user.user_id;
+ } else {
+ req.session.user_id = undefined;
+ }
+ res.send(user);
+ }).catch(function (error) {
+ res.send(error);
+ })
+});
+
+// POST user logout
+server.post('/logout', function (req, res) {
+ req.session.user_id = undefined;
+ req.session.destroy(function (err) {
+ if (err) {
+ console.error(err);
+ res.send({ error: 'Failed to end session' })
+ }
+ res.send({ success: true });
+ });
+});
+
+// GET own user info
+server.get('/users/me', function (req, res) {
+ if (!req.session.user_id) {
+ res.send({ error: 'Must be logged in' });
+ return
+ }
+
+ getUserById(req.session.user_id).then(function (user) {
+ res.send(user);
+ }).catch(function (error) {
+ res.send(error);
+ });
+});
+
+// POST generate API key
+server.post('/api/key', function (req, res) {
+ if (!req.session.user_id) {
+ res.send({ error: 'Must be logged in' });
+ return
+ }
+
+ getNewUserAPIKey(req.session.user_id).then(function (apiKey) {
+ res.send(apiKey);
+ }).catch(function (error) {
+ res.send(error);
+ });
+})
+
+// GET search
+server.get('/search', function (req, res) {
+ const searchTerm = req.query.q;
+ if (!searchTerm) {
+ res.send({
+ error: 'Please provide a search term'
+ })
+ return
+ }
+ queryLocation(searchTerm).then((results) => {
+ if (typeof (results) === 'undefined') {
+ res.send({
+ error: 'Database error'
+ })
+ return
+ }
+ res.send({
+ results: results.map(item => {
+ // map from DB results to GeoJSON Feature objects
+ const geom = JSON.parse(item.st_asgeojson)
+ return {
+ type: 'Feature',
+ attributes: {
+ label: item.search_str,
+ zoom: item.zoom || 9
+ },
+ geometry: geom
+ }
+ })
+ })
+ }).catch(function (error) {
+ res.send(error);
+ });
+})
+
+server.use((req, res) => {
+ res.status(404).json({ error: 'Resource not found'});
+})
+
+export default server;
diff --git a/app/src/api/controllers/buildingController.ts b/app/src/api/controllers/buildingController.ts
new file mode 100644
index 00000000..7987293d
--- /dev/null
+++ b/app/src/api/controllers/buildingController.ts
@@ -0,0 +1,154 @@
+import * as buildingService from '../services/building';
+import * as userService from '../services/user';
+
+
+// GET buildings
+// not implemented - may be useful to GET all buildings, paginated
+
+// GET buildings at point
+function getBuildingsByLocation(req, res) {
+ const { lng, lat } = req.query;
+ buildingService.queryBuildingsAtPoint(lng, lat).then(function (result) {
+ res.send(result);
+ }).catch(function (error) {
+ console.error(error);
+ res.send({ error: 'Database error' })
+ })
+}
+
+// GET buildings by reference (UPRN/TOID or other identifier)
+function getBuildingsByReference(req, res) {
+ const { key, id } = req.query;
+ buildingService.queryBuildingsByReference(key, id).then(function (result) {
+ res.send(result);
+ }).catch(function (error) {
+ console.error(error);
+ res.send({ error: 'Database error' })
+ })
+}
+
+// GET individual building, POST building updates
+function getBuildingById(req, res) {
+ const { building_id } = req.params;
+ buildingService.getBuildingById(building_id).then(function (result) {
+ res.send(result);
+ }).catch(function (error) {
+ console.error(error);
+ res.send({ error: 'Database error' })
+ })
+}
+
+function updateBuildingById(req, res) {
+ if (req.session.user_id) {
+ updateBuilding(req, res, req.session.user_id);
+ } else if (req.query.api_key) {
+ userService.authAPIUser(req.query.api_key)
+ .then(function (user) {
+ updateBuilding(req, res, user.user_id)
+ })
+ .catch(function (err) {
+ console.error(err);
+ res.send({ error: 'Must be logged in' });
+ });
+ } else {
+ res.send({ error: 'Must be logged in' });
+ }
+}
+
+function updateBuilding(req, res, userId) {
+ const { building_id } = req.params;
+ const building = req.body;
+ buildingService.saveBuilding(building_id, building, userId).then(building => {
+ if (building.error) {
+ res.send(building)
+ return
+ }
+ if (typeof (building) === 'undefined') {
+ res.send({ error: 'Database error' })
+ return
+ }
+ res.send(building)
+ }).catch(
+ () => res.send({ error: 'Database error' })
+ )
+}
+
+// GET building UPRNs
+function getBuildingUPRNsById(req, res) {
+ const { building_id } = req.params;
+ buildingService.getBuildingUPRNsById(building_id).then(function (result) {
+ if (typeof (result) === 'undefined') {
+ res.send({ error: 'Database error' })
+ return
+ }
+ res.send({
+ uprns: result
+ });
+ }).catch(function (error) {
+ console.error(error);
+ res.send({ error: 'Database error' })
+ })
+}
+
+// GET/POST like building
+function getBuildingLikeById(req, res) {
+ if (!req.session.user_id) {
+ res.send({ like: false }); // not logged in, so cannot have liked
+ return
+ }
+ const { building_id } = req.params;
+ buildingService.getBuildingLikeById(building_id, req.session.user_id).then(like => {
+ // any value returned means like
+ res.send({ like: like })
+ }).catch(
+ () => res.send({ error: 'Database error' })
+ )
+}
+
+function updateBuildingLikeById(req, res) {
+ if (!req.session.user_id) {
+ res.send({ error: 'Must be logged in' });
+ return
+ }
+ const { building_id } = req.params;
+ const { like } = req.body;
+ if (like) {
+ buildingService.likeBuilding(building_id, req.session.user_id).then(building => {
+ if (building.error) {
+ res.send(building)
+ return
+ }
+ if (typeof (building) === 'undefined') {
+ res.send({ error: 'Database error' })
+ return
+ }
+ res.send(building)
+ }).catch(
+ () => res.send({ error: 'Database error' })
+ )
+ } else {
+ buildingService.unlikeBuilding(building_id, req.session.user_id).then(building => {
+ if (building.error) {
+ res.send(building)
+ return
+ }
+ if (typeof (building) === 'undefined') {
+ res.send({ error: 'Database error' })
+ return
+ }
+ res.send(building)
+ }).catch(
+ () => res.send({ error: 'Database error' })
+ )
+ }
+}
+
+export default {
+ getBuildingsByLocation,
+ getBuildingsByReference,
+ getBuildingById,
+ updateBuildingById,
+ getBuildingUPRNsById,
+ getBuildingLikeById,
+ updateBuildingLikeById
+};
\ No newline at end of file
diff --git a/app/src/api/routes/buildingsRouter.ts b/app/src/api/routes/buildingsRouter.ts
new file mode 100644
index 00000000..a8921ab6
--- /dev/null
+++ b/app/src/api/routes/buildingsRouter.ts
@@ -0,0 +1,32 @@
+import express from 'express';
+
+import buildingController from '../controllers/buildingController';
+
+const router = express.Router();
+
+
+// GET buildings
+// not implemented - may be useful to GET all buildings, paginated
+
+// GET buildings at point
+router.get('/locate', buildingController.getBuildingsByLocation);
+
+// GET buildings by reference (UPRN/TOID or other identifier)
+router.get('/reference', buildingController.getBuildingsByReference);
+
+router.route('/:building_id.json')
+ // GET individual building
+ .get(buildingController.getBuildingById)
+ // POST building updates
+ .post(buildingController.updateBuildingById);
+
+
+// GET building UPRNs
+router.get('/:building_id/uprns.json', buildingController.getBuildingUPRNsById);
+
+// GET/POST like building
+router.route('/:building_id/like.json')
+ .get(buildingController.getBuildingLikeById)
+ .post(buildingController.updateBuildingLikeById);
+
+export default router;
\ No newline at end of file
diff --git a/app/src/api/building.ts b/app/src/api/services/building.ts
similarity index 99%
rename from app/src/api/building.ts
rename to app/src/api/services/building.ts
index 39bf8d10..4d4befcc 100644
--- a/app/src/api/building.ts
+++ b/app/src/api/services/building.ts
@@ -2,8 +2,8 @@
* Building data access
*
*/
-import db from '../db';
-import { removeAllAtBbox } from '../tiles/cache';
+import db from '../../db';
+import { removeAllAtBbox } from '../../tiles/cache';
// data type note: PostgreSQL bigint (64-bit) is handled as string in JavaScript, because of
// JavaScript numerics are 64-bit double, giving only partial coverage.
diff --git a/app/src/api/search.ts b/app/src/api/services/search.ts
similarity index 96%
rename from app/src/api/search.ts
rename to app/src/api/services/search.ts
index 08fbebe3..0defa117 100644
--- a/app/src/api/search.ts
+++ b/app/src/api/services/search.ts
@@ -6,7 +6,7 @@
* - this DOES expose geometry, another reason to keep this clearly separated from building
* data
*/
-import db from '../db';
+import db from '../../db';
function queryLocation(term) {
const limit = 5;
diff --git a/app/src/api/user.ts b/app/src/api/services/user.ts
similarity index 99%
rename from app/src/api/user.ts
rename to app/src/api/services/user.ts
index 7cb3b6c2..da7e2e5d 100644
--- a/app/src/api/user.ts
+++ b/app/src/api/services/user.ts
@@ -2,7 +2,7 @@
* User data access
*
*/
-import db from '../db';
+import db from '../../db';
function createUser(user) {
if (!user.password || user.password.length < 8) {
diff --git a/app/src/frontend/app.tsx b/app/src/frontend/app.tsx
index 0c824689..c060313b 100644
--- a/app/src/frontend/app.tsx
+++ b/app/src/frontend/app.tsx
@@ -85,7 +85,7 @@ class App extends React.Component